feat: Update camera dependencies and implement selfie verification UI
- Updated camera package version to 0.11.0+2 and camera_android_camerax to 0.6.7+2 in pubspec.lock and pubspec.yaml. - Removed unused google_ml_kit dependencies from pubspec.lock and pubspec.yaml. - Added CameraPreviewWidget for displaying camera feed in registration form. - Created CapturedSelfieView to show the captured selfie and verification status. - Implemented Debug Panel for liveness detection with real-time status updates. - Added ErrorStateWidget to handle and display error messages during verification. - Introduced InstructionBanner to guide users through the verification process. - Developed VerificationProgressWidget to visually represent the verification progress.
This commit is contained in:
parent
790eb4325d
commit
5dc7aa3cc7
|
@ -1140,3 +1140,251 @@ class FaceLivenessController extends GetxController
|
|||
return orientation == Orientation.portrait ? "Portrait" : "Landscape";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CameraScreen extends StatefulWidget {
|
||||
const CameraScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CameraScreen> createState() => _CameraScreenState();
|
||||
}
|
||||
|
||||
class _CameraScreenState extends State<CameraScreen>
|
||||
with WidgetsBindingObserver, TickerProviderStateMixin {
|
||||
CameraController? controller;
|
||||
String error='';
|
||||
|
||||
FaceDetector faceDetector = FaceDetector(
|
||||
options: FaceDetectorOptions(
|
||||
enableClassification: true,
|
||||
enableContours: true,
|
||||
enableTracking: true,
|
||||
enableLandmarks: true,
|
||||
performanceMode: Platform.isAndroid
|
||||
? FaceDetectorMode.fast
|
||||
: FaceDetectorMode.accurate,
|
||||
),
|
||||
);
|
||||
|
||||
final orientations = {
|
||||
DeviceOrientation.portraitUp: 0,
|
||||
DeviceOrientation.landscapeLeft: 90,
|
||||
DeviceOrientation.portraitDown: 180,
|
||||
DeviceOrientation.landscapeRight: 270,
|
||||
};
|
||||
|
||||
initialize() async {
|
||||
controller = CameraController(
|
||||
cameras[1],
|
||||
ResolutionPreset.medium,
|
||||
imageFormatGroup: Platform.isAndroid
|
||||
? ImageFormatGroup.nv21
|
||||
: ImageFormatGroup.bgra8888,
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
initializeCameraController(controller!.description);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
initialize();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
faceDetector.close();
|
||||
if (controller != null) controller!.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
final CameraController? cameraController = controller;
|
||||
if (cameraController == null || !cameraController.value.isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == AppLifecycleState.inactive) {
|
||||
cameraController.dispose();
|
||||
} else if (state == AppLifecycleState.resumed) {
|
||||
initialize();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initializeCameraController() async {
|
||||
try {
|
||||
await controller!.initialize();
|
||||
controller!.setFlashMode(FlashMode.off);
|
||||
// If the controller is updated then update the UI.
|
||||
controller!.addListener(() {
|
||||
if (mounted) setState(() {});
|
||||
controller!.startImageStream(_processCameraImage).then((value) {});
|
||||
});
|
||||
if (mounted) setState(() {});
|
||||
} on CameraException catch (e) {
|
||||
switch (e.code) {
|
||||
case 'CameraAccessDenied':
|
||||
Get.back();
|
||||
cameraDialog();
|
||||
break;
|
||||
case 'CameraAccessDeniedWithoutPrompt':
|
||||
Get.back();
|
||||
cameraDialog();
|
||||
break;
|
||||
case 'CameraAccessRestricted':
|
||||
Get.back();
|
||||
cameraDialog();
|
||||
break;
|
||||
default:
|
||||
Get.back();
|
||||
EasyLoading.showError(e.description!);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
cameraDialog() {
|
||||
Get.defaultDialog(
|
||||
title: 'Camera Access',
|
||||
middleText:
|
||||
'Looks like you have not provided permission to camera completely. Enable it in the settings.',
|
||||
onConfirm: () async {
|
||||
Get.back();
|
||||
await openAppSettings();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
clearCaptured(String message) {
|
||||
debugPrint(message);
|
||||
imageFile = null;
|
||||
error = message;
|
||||
if (mounted) setState(() {});
|
||||
return;
|
||||
}
|
||||
|
||||
void _processCameraImage(CameraImage image) {
|
||||
final inputImage = inputImageFromCameraImage(image);
|
||||
if (inputImage == null) return;
|
||||
checkImage(inputImage);
|
||||
}
|
||||
|
||||
checkImage(InputImage inputImage) async {
|
||||
List<Face> faces = await faceDetector.processImage(inputImage);
|
||||
debugPrint('faces: $faces');
|
||||
if (faces.isEmpty) {
|
||||
clearCaptured('No faces detected in the image.');
|
||||
}
|
||||
if (faces.length > 1) {
|
||||
clearCaptured(
|
||||
'Multiple faces detected. Please try capturing a single face.',
|
||||
);
|
||||
}
|
||||
// Handle the detected faces
|
||||
for (Face face in faces) {
|
||||
if (face.leftEyeOpenProbability != null &&
|
||||
face.rightEyeOpenProbability != null) {
|
||||
final leftEyeOpen = face.leftEyeOpenProbability! > 0.5;
|
||||
final rightEyeOpen = face.rightEyeOpenProbability! > 0.5;
|
||||
if (leftEyeOpen && rightEyeOpen) {
|
||||
error = 'Click capture to save this image';
|
||||
if (mounted) setState(() {});
|
||||
return;
|
||||
} else if (!leftEyeOpen && !rightEyeOpen) {
|
||||
clearCaptured('Both eyes are closed!');
|
||||
} else if (leftEyeOpen && !rightEyeOpen) {
|
||||
clearCaptured('Left eye is open, right eye is closed.');
|
||||
} else if (!leftEyeOpen && rightEyeOpen) {
|
||||
clearCaptured('Right eye is open, left eye is closed.');
|
||||
}
|
||||
} else {
|
||||
clearCaptured(
|
||||
'Looks like either one or both eyes have not been captured properly. Please try again.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
InputImage? inputImageFromCameraImage(CameraImage image) {
|
||||
final camera = cameras[1];
|
||||
final sensorOrientation = camera.sensorOrientation;
|
||||
InputImageRotation? rotation;
|
||||
if (Platform.isIOS) {
|
||||
rotation = InputImageRotationValue.fromRawValue(sensorOrientation);
|
||||
} else if (Platform.isAndroid) {
|
||||
var rotationCompensation =
|
||||
orientations[controller!.value.deviceOrientation];
|
||||
if (rotationCompensation == null) return null;
|
||||
if (camera.lensDirection == CameraLensDirection.front) {
|
||||
// front-facing
|
||||
rotationCompensation = (sensorOrientation + rotationCompensation) % 360;
|
||||
} else {
|
||||
// back-facing
|
||||
rotationCompensation =
|
||||
(sensorOrientation - rotationCompensation + 360) % 360;
|
||||
}
|
||||
rotation = InputImageRotationValue.fromRawValue(rotationCompensation);
|
||||
}
|
||||
if (rotation == null) return null;
|
||||
|
||||
// get image format
|
||||
final format = InputImageFormatValue.fromRawValue(image.format.raw);
|
||||
|
||||
if (format == null ||
|
||||
(Platform.isAndroid && format != InputImageFormat.nv21) ||
|
||||
(Platform.isIOS && format != InputImageFormat.bgra8888)) return null;
|
||||
|
||||
// since format is constraint to nv21 or bgra8888, both only have one plane
|
||||
if (image.planes.length != 1) return null;
|
||||
final plane = image.planes.first;
|
||||
|
||||
// compose InputImage using bytes
|
||||
return InputImage.fromBytes(
|
||||
bytes: plane.bytes,
|
||||
metadata: InputImageMetadata(
|
||||
size: Size(image.width.toDouble(), image.height.toDouble()),
|
||||
rotation: rotation, // used only in Android
|
||||
format: format, // used only in iOS
|
||||
bytesPerRow: plane.bytesPerRow, // used only in iOS
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text('Capture Your Selfie'),
|
||||
),
|
||||
bottomNavigationBar: controller == null
|
||||
? null
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(error),
|
||||
SizedBox(height:30),
|
||||
ElevatedButton(
|
||||
child: Text('Capture'),
|
||||
onPressed: () async {
|
||||
await controller!.takePicture().then((value) {});
|
||||
},
|
||||
),
|
||||
SizedBox(height:20),
|
||||
],
|
||||
),
|
||||
body: controller == null
|
||||
? Center(child: Text('Need to access camera to capture selfie'))
|
||||
: CameraPreview(controller!),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,75 +1,77 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Sigap</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>sigap</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<!-- Add this array for Deep Links -->
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>io.supabase.flutterquickstart</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<!-- ... other tags -->
|
||||
<!-- <key>NSLocationWhenInUseUsageDescription</key>
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Sigap</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>sigap</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true />
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true />
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true />
|
||||
<!-- Add this array for Deep Links -->
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>io.supabase.flutterquickstart</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<!-- ... other tags -->
|
||||
<!-- <key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>This app needs access to location when open.</string> -->
|
||||
<!-- Mapbox -->
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Aplikasi ini memerlukan akses lokasi untuk menunjukkan posisi Anda pada peta</string>
|
||||
<key>NSLocationAlwaysUsageDescription</key>
|
||||
<string>Aplikasi ini memerlukan akses lokasi untuk navigasi latar belakang</string>
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true/>
|
||||
<key>MGLMapboxAccessToken</key>
|
||||
<string>$(MAPBOX_ACCESS_TOKEN)</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
<!-- Mapbox -->
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Aplikasi ini memerlukan akses lokasi untuk menunjukkan posisi Anda pada peta</string>
|
||||
<key>NSLocationAlwaysUsageDescription</key>
|
||||
<string>Aplikasi ini memerlukan akses lokasi untuk navigasi latar belakang</string>
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true />
|
||||
<key>MGLMapboxAccessToken</key>
|
||||
<string>$(MAPBOX_ACCESS_TOKEN)</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We need access to your camera to detect faces.</string>
|
||||
<false />
|
||||
</dict>
|
||||
</plist>
|
|
@ -3,7 +3,7 @@ import 'dart:developer' as dev;
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
||||
import 'package:google_mlkit_face_mesh_detection/google_mlkit_face_mesh_detection.dart';
|
||||
|
@ -23,6 +23,13 @@ enum LivenessStatus {
|
|||
failed,
|
||||
}
|
||||
|
||||
final orientations = {
|
||||
DeviceOrientation.portraitUp: 0,
|
||||
DeviceOrientation.landscapeLeft: 90,
|
||||
DeviceOrientation.portraitDown: 180,
|
||||
DeviceOrientation.landscapeRight: 270,
|
||||
};
|
||||
|
||||
class FaceLivenessController extends GetxController {
|
||||
// Camera
|
||||
CameraController? cameraController;
|
||||
|
@ -201,9 +208,10 @@ class FaceLivenessController extends GetxController {
|
|||
|
||||
// Detect faces
|
||||
final faces = await faceDetector.processImage(inputImage);
|
||||
|
||||
// Process face detection results
|
||||
await _processFaceDetection(faces);
|
||||
|
||||
dev.log('Detected ${faces.length} faces', name: 'LIVENESS_CONTROLLER');
|
||||
} catch (e) {
|
||||
dev.log('Error processing image: $e', name: 'LIVENESS_CONTROLLER');
|
||||
} finally {
|
||||
|
@ -233,7 +241,18 @@ class FaceLivenessController extends GetxController {
|
|||
if (Platform.isIOS) {
|
||||
rotation = InputImageRotationValue.fromRawValue(sensorOrientation);
|
||||
} else if (Platform.isAndroid) {
|
||||
var rotationCompensation = sensorOrientation;
|
||||
var rotationCompensation =
|
||||
orientations[cameraController!.value.deviceOrientation];
|
||||
if (rotationCompensation == null) return null;
|
||||
if (camera.lensDirection == CameraLensDirection.front) {
|
||||
// front-facing
|
||||
rotationCompensation =
|
||||
(sensorOrientation + rotationCompensation) % 360;
|
||||
} else {
|
||||
// back-facing
|
||||
rotationCompensation =
|
||||
(sensorOrientation - rotationCompensation + 360) % 360;
|
||||
}
|
||||
rotation = InputImageRotationValue.fromRawValue(rotationCompensation);
|
||||
}
|
||||
|
||||
|
@ -241,15 +260,11 @@ class FaceLivenessController extends GetxController {
|
|||
dev.log('Could not determine rotation', name: 'LIVENESS_CONTROLLER');
|
||||
return null;
|
||||
}
|
||||
|
||||
final format = InputImageFormatValue.fromRawValue(image.format.raw);
|
||||
if (format == null) {
|
||||
dev.log(
|
||||
'Unsupported image format: ${image.format.raw}',
|
||||
name: 'LIVENESS_CONTROLLER',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final format =
|
||||
Platform.isAndroid
|
||||
? InputImageFormat.nv21
|
||||
: InputImageFormat.bgra8888;
|
||||
|
||||
// Handle different plane configurations
|
||||
if (image.planes.isEmpty) {
|
||||
|
@ -469,7 +484,7 @@ class FaceLivenessController extends GetxController {
|
|||
// Capture image with retry logic
|
||||
int retryCount = 0;
|
||||
const maxRetries = 3;
|
||||
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
try {
|
||||
capturedImage = await cameraController!.takePicture();
|
||||
|
@ -480,11 +495,11 @@ class FaceLivenessController extends GetxController {
|
|||
'Capture attempt $retryCount failed: $e',
|
||||
name: 'LIVENESS_CONTROLLER',
|
||||
);
|
||||
|
||||
|
||||
if (retryCount >= maxRetries) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
|
||||
// Wait before retry
|
||||
await Future.delayed(Duration(milliseconds: 500));
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,60 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class CameraPreviewWidget extends StatelessWidget {
|
||||
final FaceLivenessController controller;
|
||||
final double screenWidth;
|
||||
|
||||
const CameraPreviewWidget({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.screenWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Camera background
|
||||
Container(
|
||||
width: screenWidth * 0.85,
|
||||
height: screenWidth * 0.85,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
),
|
||||
|
||||
// Camera preview
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: SizedBox(
|
||||
width: screenWidth * 0.85,
|
||||
height: screenWidth * 0.85,
|
||||
child: controller.cameraController!.buildPreview(),
|
||||
),
|
||||
),
|
||||
|
||||
// Scanning animation
|
||||
Positioned(
|
||||
top: 0,
|
||||
child: Container(
|
||||
width: screenWidth * 0.65,
|
||||
height: 2,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
TColors.primary.withOpacity(0.8),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
import 'dart:developer' as dev;
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class CapturedSelfieView extends StatelessWidget {
|
||||
final FaceLivenessController controller;
|
||||
final SelfieVerificationController? selfieController;
|
||||
|
||||
const CapturedSelfieView({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.selfieController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.white, Colors.green.shade50],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Success icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check_circle_outline,
|
||||
color: Colors.green.shade600,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Text(
|
||||
'Verification Successful!',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green.shade700,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Text(
|
||||
'Your identity has been verified',
|
||||
style: TextStyle(fontSize: 16, color: Colors.black54),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Display captured image
|
||||
if (controller.capturedImage != null)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(150),
|
||||
border: Border.all(color: Colors.white, width: 4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(150),
|
||||
child: Image.file(
|
||||
File(controller.capturedImage!.path),
|
||||
width: 200,
|
||||
height: 200,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Completed steps list
|
||||
_buildCompletedStepsList(),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Continue button - clear loading state properly
|
||||
ElevatedButton(
|
||||
onPressed: () => _handleContinueButton(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: TColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(double.infinity, 56),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text(
|
||||
'Continue',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Build the completed steps list
|
||||
Widget _buildCompletedStepsList() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.verified, color: Colors.green.shade600, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'All verification steps completed',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
...controller.successfulSteps.map(
|
||||
(step) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
color: Colors.green.shade600,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
step,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.black87),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Handle the continue button
|
||||
void _handleContinueButton() {
|
||||
// Reset loading state in selfie controller before navigating back
|
||||
try {
|
||||
if (selfieController != null) {
|
||||
dev.log(
|
||||
'Found SelfieVerificationController, handling success',
|
||||
name: 'LIVENESS_DEBUG',
|
||||
);
|
||||
// Connect with SelfieVerificationController
|
||||
if (controller.capturedImage != null) {
|
||||
dev.log(
|
||||
'Setting captured image on SelfieVerificationController',
|
||||
name: 'LIVENESS_DEBUG',
|
||||
);
|
||||
selfieController?.selfieImage.value = controller.capturedImage;
|
||||
// selfieController._processCapturedLivenessImage();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
dev.log(
|
||||
'Error connecting with SelfieVerificationController: $e',
|
||||
name: 'LIVENESS_DEBUG',
|
||||
);
|
||||
// Continue without selfie controller
|
||||
}
|
||||
Get.back(result: controller.capturedImage);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,333 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart';
|
||||
|
||||
/// Shows the debug panel for liveness detection
|
||||
void showLivenessDebugPanel(
|
||||
BuildContext context,
|
||||
FaceLivenessController controller,
|
||||
SelfieVerificationController? selfieController,
|
||||
) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder:
|
||||
(context) => LivenessDebugPanel(
|
||||
controller: controller,
|
||||
selfieController: selfieController,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class LivenessDebugPanel extends StatelessWidget {
|
||||
final FaceLivenessController controller;
|
||||
final SelfieVerificationController? selfieController;
|
||||
|
||||
const LivenessDebugPanel({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.selfieController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Liveness Detection Debug Panel',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Obx(
|
||||
() => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Status section
|
||||
Text(
|
||||
'Camera & Detection Status',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_debugItem(
|
||||
'Camera Controller',
|
||||
'${controller.cameraController?.value.isInitialized}',
|
||||
),
|
||||
_debugItem('Status', '${controller.status.value}'),
|
||||
_debugItem(
|
||||
'Face In Frame',
|
||||
'${controller.isFaceInFrame.value}',
|
||||
),
|
||||
_debugItem('Face Left', '${controller.isFaceLeft}'),
|
||||
_debugItem('Face Right', '${controller.isFaceRight}'),
|
||||
_debugItem('Eyes Open', '${controller.isEyeOpen}'),
|
||||
_debugItem('Smiled', '${controller.isSmiled}'),
|
||||
_debugItem(
|
||||
'Ready For Photo',
|
||||
'${controller.isFaceReadyForPhoto.value}',
|
||||
),
|
||||
_debugItem('Captured', '${controller.isCaptured}'),
|
||||
_debugItem(
|
||||
'Steps Completed',
|
||||
'${controller.successfulSteps.length}',
|
||||
),
|
||||
_debugItem('Steps', '${controller.successfulSteps}'),
|
||||
|
||||
Divider(),
|
||||
Text(
|
||||
'Selfie Controller',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_debugItem('Found', '${selfieController != null}'),
|
||||
|
||||
if (selfieController != null) ...[
|
||||
_debugItem(
|
||||
'Is Performing Check',
|
||||
'${selfieController?.isPerformingLivenessCheck.value}',
|
||||
),
|
||||
_debugItem(
|
||||
'Is Selfie Valid',
|
||||
'${selfieController?.isSelfieValid.value}',
|
||||
),
|
||||
_debugItem(
|
||||
'Has Confirmed Selfie',
|
||||
'${selfieController?.hasConfirmedSelfie.value}',
|
||||
),
|
||||
_debugItem(
|
||||
'Liveness Passed',
|
||||
'${selfieController?.isLivenessCheckPassed.value}',
|
||||
),
|
||||
],
|
||||
|
||||
Divider(),
|
||||
_buildQuickActionsSection(context),
|
||||
|
||||
SizedBox(height: 16),
|
||||
// Additional test connection button
|
||||
if (selfieController != null &&
|
||||
controller.capturedImage != null)
|
||||
OutlinedButton.icon(
|
||||
icon: Icon(Icons.link),
|
||||
label: Text('Test Connection to Selfie Controller'),
|
||||
onPressed: () {
|
||||
selfieController?.selfieImage.value =
|
||||
controller.capturedImage;
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Test data sent to SelfieVerificationController',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Quick actions section
|
||||
Widget _buildQuickActionsSection(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.amber.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Quick Actions',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: Colors.amber.shade800,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
'Use these actions to bypass steps or debug issues:',
|
||||
style: TextStyle(fontSize: 14, color: Colors.black87),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Button group - layout for buttons
|
||||
GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
childAspectRatio: 2.5,
|
||||
children: [
|
||||
// Skip all steps button
|
||||
ElevatedButton.icon(
|
||||
icon: Icon(Icons.skip_next, color: Colors.white, size: 18),
|
||||
label: Text(
|
||||
'Skip All Steps',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
controller.skipAllVerificationSteps();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Skipping all steps'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Force next step button
|
||||
ElevatedButton.icon(
|
||||
icon: Icon(Icons.arrow_forward, color: Colors.white, size: 18),
|
||||
label: Text(
|
||||
'Next Step',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
controller.forceAdvanceToNextStep();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Forced next step'),
|
||||
backgroundColor: Colors.amber,
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.amber,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Force capture button
|
||||
ElevatedButton.icon(
|
||||
icon: Icon(Icons.camera, color: Colors.white, size: 18),
|
||||
label: Text(
|
||||
'Force Capture',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
if (controller.cameraController != null &&
|
||||
controller.cameraController!.value.isInitialized) {
|
||||
controller.forceCaptureImage();
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Camera not ready'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Reset process button
|
||||
ElevatedButton.icon(
|
||||
icon: Icon(Icons.refresh, color: Colors.white, size: 18),
|
||||
label: Text(
|
||||
'Reset Process',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
controller.resetProcess();
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Process reset'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Debug list item
|
||||
Widget _debugItem(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Text('$label: ', style: TextStyle(fontWeight: FontWeight.w500)),
|
||||
Expanded(child: Text(value, style: TextStyle(color: Colors.blue))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class ErrorStateWidget extends StatelessWidget {
|
||||
final String message;
|
||||
|
||||
const ErrorStateWidget({
|
||||
Key? key,
|
||||
required this.message,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String userFriendlyMessage = message;
|
||||
|
||||
// Convert technical errors to user-friendly messages
|
||||
if (message.contains('server_config_error') ||
|
||||
message.contains('environment variables')) {
|
||||
userFriendlyMessage =
|
||||
'The face verification service is temporarily unavailable. Please try again later.';
|
||||
} else if (message.contains('network') || message.contains('connection')) {
|
||||
userFriendlyMessage =
|
||||
'Network error. Please check your internet connection and try again.';
|
||||
} else if (message.contains('timeout')) {
|
||||
userFriendlyMessage =
|
||||
'The request timed out. Please try again when you have a stronger connection.';
|
||||
} else if (message.contains('Camera initialization failed')) {
|
||||
userFriendlyMessage =
|
||||
'Unable to access camera. Please check your camera permissions and try again.';
|
||||
} else if (message.contains('decode') ||
|
||||
message.contains('Body can not be decoded')) {
|
||||
userFriendlyMessage =
|
||||
'There was a problem processing your image. Please try again.';
|
||||
} else if (message.contains('invalid_request_format')) {
|
||||
userFriendlyMessage =
|
||||
'There was a problem with the image format. Please try again with a different image.';
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: Colors.red, size: 48),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Verification Error',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Text(
|
||||
userFriendlyMessage,
|
||||
style: TextStyle(color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: TColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: Text('Go Back'),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Reset and try again
|
||||
try {
|
||||
final controller = Get.find<FaceLivenessController>();
|
||||
controller.resetProcess();
|
||||
} catch (e) {
|
||||
// Handle case where controller isn't available
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
child: Text('Try Again'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class InstructionBanner extends StatelessWidget {
|
||||
final FaceLivenessController controller;
|
||||
|
||||
const InstructionBanner({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: TColors.primary.withOpacity(0.08),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: TColors.primary.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.face_retouching_natural,
|
||||
color: TColors.primary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
controller.getCurrentDirection(),
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: TColors.primary,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Status indicator
|
||||
Obx(
|
||||
() => Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(controller.status.value),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusText(controller.status.value),
|
||||
style: TextStyle(color: Colors.white, fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Function to get status color
|
||||
Color _getStatusColor(LivenessStatus status) {
|
||||
switch (status) {
|
||||
case LivenessStatus.preparing:
|
||||
case LivenessStatus.detectingFace:
|
||||
return Colors.orange;
|
||||
case LivenessStatus.failed:
|
||||
return Colors.red;
|
||||
case LivenessStatus.completed:
|
||||
case LivenessStatus.photoTaken:
|
||||
return Colors.green;
|
||||
default:
|
||||
return TColors.primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to get status text
|
||||
String _getStatusText(LivenessStatus status) {
|
||||
switch (status) {
|
||||
case LivenessStatus.preparing:
|
||||
return 'Preparing';
|
||||
case LivenessStatus.detectingFace:
|
||||
return 'Detecting';
|
||||
case LivenessStatus.checkLeftRotation:
|
||||
return 'Look Left';
|
||||
case LivenessStatus.checkRightRotation:
|
||||
return 'Look Right';
|
||||
case LivenessStatus.checkSmile:
|
||||
return 'Smile';
|
||||
case LivenessStatus.checkEyesOpen:
|
||||
return 'Open Eyes';
|
||||
case LivenessStatus.readyForPhoto:
|
||||
return 'Ready';
|
||||
case LivenessStatus.photoTaken:
|
||||
return 'Processing';
|
||||
case LivenessStatus.completed:
|
||||
return 'Success';
|
||||
case LivenessStatus.failed:
|
||||
return 'Failed';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class VerificationProgressWidget extends StatelessWidget {
|
||||
final FaceLivenessController controller;
|
||||
|
||||
const VerificationProgressWidget({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 0,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.verified_outlined, color: TColors.primary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Verification Progress',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Progress indicator
|
||||
Obx(
|
||||
() => LinearProgressIndicator(
|
||||
value: controller.successfulSteps.length / 4,
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
color: TColors.primary,
|
||||
minHeight: 6,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Steps list
|
||||
Obx(() {
|
||||
if (controller.successfulSteps.isEmpty) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
'Follow the instructions to complete verification',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black54,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children:
|
||||
controller.successfulSteps
|
||||
.map(
|
||||
(step) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
color: Colors.green.shade600,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
step,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -109,18 +109,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: camera
|
||||
sha256: "413d2b34fe28496c35c69ede5b232fb9dd5ca2c3a4cb606b14efc1c7546cc8cb"
|
||||
sha256: "26ff41045772153f222ffffecba711a206f670f5834d40ebf5eed3811692f167"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
version: "0.11.0+2"
|
||||
camera_android_camerax:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: camera_android_camerax
|
||||
sha256: "9fb44e73e0fea3647a904dc26d38db24055e5b74fc68fd2b6d3abfa1bd20f536"
|
||||
sha256: "8bd9cab67551642eb33ceb33ece7acc0890014fc90ddfae637c7e2b683657e65"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.17"
|
||||
version: "0.6.7+2"
|
||||
camera_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -693,22 +693,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+1"
|
||||
google_ml_kit:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_ml_kit
|
||||
sha256: a2da12a62353a6cad71534b52ada3af14a5b842e6c9b1014ce4d243652b30f4b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.0"
|
||||
google_mlkit_barcode_scanning:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_mlkit_barcode_scanning
|
||||
sha256: b38505df2d3fdf7830979d60fee55039c2f442d189b2e06fcb2fe494ba65d0db
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.1"
|
||||
google_mlkit_commons:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -717,22 +701,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.0"
|
||||
google_mlkit_digital_ink_recognition:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_mlkit_digital_ink_recognition
|
||||
sha256: "8d2b89401bdeeba97158377167429dbc5cb339ebbd21e0889dca773f1c79a884"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.1"
|
||||
google_mlkit_entity_extraction:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_mlkit_entity_extraction
|
||||
sha256: "145bc26422b7e62d50cc4eca1ac394d13ac6a97e4c09b8baf7ff058b64d2f9cc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.1"
|
||||
google_mlkit_face_detection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -749,70 +717,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
google_mlkit_image_labeling:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_mlkit_image_labeling
|
||||
sha256: "2cac5f7a02dcc23cd3357f89bf1a79df793ae3afce5035a896de467ffa0192e8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.1"
|
||||
google_mlkit_language_id:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_mlkit_language_id
|
||||
sha256: fc57bca69cb1dcd8ef67b929f0315e9a8baa80c03c75f7a1226becd7ad2529ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
google_mlkit_object_detection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_mlkit_object_detection
|
||||
sha256: "0f740f046d74faf81d9c44cdbe4accf33888ed9f877e30efbfad4578d45ebfcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.0"
|
||||
google_mlkit_pose_detection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_mlkit_pose_detection
|
||||
sha256: "5ff5fe2a325427c49c02a884a2a888d2d10cbfe414f7ebf2af9777a5155171eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.0"
|
||||
google_mlkit_selfie_segmentation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_mlkit_selfie_segmentation
|
||||
sha256: e05fc255265595a0fb11cd6a6a5393f106d6ec4d3a40cbc57ff22894eef235f1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.0"
|
||||
google_mlkit_smart_reply:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_mlkit_smart_reply
|
||||
sha256: "0c3d737e46f20aa4d4953860ee5757e5250e58f90351f8e2afdeb1d609c7047e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
google_mlkit_text_recognition:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_mlkit_text_recognition
|
||||
sha256: "96173ad4dd7fd06c660e22ac3f9e9f1798a517fe7e48bee68eeec83853224224"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.0"
|
||||
google_mlkit_translation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_mlkit_translation
|
||||
sha256: "7287444a0abd994087a0b354dee952fcd198e57619ded4bba65496d418c9d84b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
google_sign_in:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
name: sigap
|
||||
description: 'A mobile app for SIGAP Jember'
|
||||
description: "A mobile app for SIGAP Jember"
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
|
@ -26,98 +26,97 @@ dependencies:
|
|||
sdk: flutter
|
||||
|
||||
# --- Date & Time Utilities ---
|
||||
intl: ^0.19.0
|
||||
timeago:
|
||||
time_slot:
|
||||
calendar_date_picker2:
|
||||
easy_date_timeline:
|
||||
intl: ^0.19.0
|
||||
timeago:
|
||||
time_slot:
|
||||
calendar_date_picker2:
|
||||
easy_date_timeline:
|
||||
equatable: ^2.0.7
|
||||
camera:
|
||||
camera: 0.11.0+2
|
||||
camera_android_camerax: 0.6.7+2
|
||||
|
||||
# --- Logging & Debugging ---
|
||||
logger:
|
||||
logger:
|
||||
|
||||
# --- URL & Launcher ---
|
||||
url_launcher:
|
||||
url_launcher:
|
||||
|
||||
# --- Splash & Icons ---
|
||||
flutter_launcher_icons:
|
||||
flutter_launcher_icons:
|
||||
animated_splash_screen: ^1.3.0
|
||||
flutter_native_splash: ^2.4.6
|
||||
flutter_tabler_icons: ^1.43.0
|
||||
|
||||
# --- UI & Animation ---
|
||||
smooth_page_indicator:
|
||||
lottie:
|
||||
shimmer:
|
||||
badges:
|
||||
carousel_slider:
|
||||
flutter_rating_bar:
|
||||
readmore:
|
||||
dropdown_search:
|
||||
dotted_border:
|
||||
smooth_page_indicator:
|
||||
lottie:
|
||||
shimmer:
|
||||
badges:
|
||||
carousel_slider:
|
||||
flutter_rating_bar:
|
||||
readmore:
|
||||
dropdown_search:
|
||||
dotted_border:
|
||||
flutter_svg: ^2.1.0
|
||||
|
||||
|
||||
# --- Input & Forms ---
|
||||
flutter_otp_text_field:
|
||||
image_picker:
|
||||
file_picker:
|
||||
flutter_otp_text_field:
|
||||
image_picker:
|
||||
file_picker:
|
||||
|
||||
# --- Storage & Filesystem ---
|
||||
path:
|
||||
path_provider:
|
||||
path:
|
||||
path_provider:
|
||||
flutter_secure_storage:
|
||||
|
||||
# --- Data & Utilities ---
|
||||
uuid:
|
||||
crypto:
|
||||
uuid:
|
||||
crypto:
|
||||
|
||||
# --- Notifications & Permissions ---
|
||||
flutter_local_notifications:
|
||||
permission_handler:
|
||||
flutter_local_notifications:
|
||||
permission_handler:
|
||||
|
||||
# --- Environment & Config ---
|
||||
flutter_dotenv:
|
||||
flutter_dotenv:
|
||||
|
||||
# --- Connectivity ---
|
||||
connectivity_plus:
|
||||
connectivity_plus:
|
||||
|
||||
# --- Map & Location ---
|
||||
mapbox_maps_flutter:
|
||||
mapbox_maps_flutter:
|
||||
|
||||
# mapbox_gl:
|
||||
polyline_do:
|
||||
flutter_polyline_points:
|
||||
# mapbox_gl:
|
||||
polyline_do:
|
||||
flutter_polyline_points:
|
||||
location:
|
||||
latlong2:
|
||||
geolocator:
|
||||
latlong2:
|
||||
geolocator:
|
||||
geocoding:
|
||||
|
||||
# --- Icons ---
|
||||
iconsax:
|
||||
cupertino_icons:
|
||||
font_awesome_flutter:
|
||||
iconsax:
|
||||
cupertino_icons:
|
||||
font_awesome_flutter:
|
||||
|
||||
# --- State Management & Storage ---
|
||||
get:
|
||||
get_storage:
|
||||
get:
|
||||
get_storage:
|
||||
|
||||
# --- Supabase & API Services ---
|
||||
supabase_flutter:
|
||||
dio:
|
||||
supabase_flutter:
|
||||
dio:
|
||||
|
||||
# --- Authentication ---
|
||||
google_sign_in:
|
||||
local_auth:
|
||||
|
||||
# --- Fonts ---
|
||||
google_fonts:
|
||||
google_fonts:
|
||||
|
||||
# --- Machine Learning ---
|
||||
google_mlkit_face_detection: ^0.13.1
|
||||
google_mlkit_face_mesh_detection: ^0.4.1
|
||||
google_ml_kit: ^0.20.0
|
||||
|
||||
# --- Localization ---
|
||||
# (add localization dependencies here if needed)
|
||||
|
@ -143,9 +142,9 @@ flutter:
|
|||
|
||||
#--------------- LOCAL ASSETS ------------------#
|
||||
assets:
|
||||
# Environment variables
|
||||
# Environment variables
|
||||
- .env
|
||||
# Images
|
||||
# Images
|
||||
- assets/logos/
|
||||
- assets/icons/brands/
|
||||
- assets/images/content/
|
||||
|
@ -155,7 +154,7 @@ flutter:
|
|||
- assets/icons/categories/
|
||||
- assets/images/animations/
|
||||
- assets/images/on_boarding_images/
|
||||
# JSON Files
|
||||
# JSON Files
|
||||
- public/jsons/provinsi.json
|
||||
- public/jsons/kabupaten_kota.json
|
||||
- public/jsons/kecamatan.json
|
||||
|
|
Loading…
Reference in New Issue