From b94d8f50a46adba4f85f9563dc57bc7d48af7266 Mon Sep 17 00:00:00 2001 From: Gaurang Khanolkar Date: Fri, 5 Jun 2026 19:37:42 +0530 Subject: [PATCH] UI: polish App.tsx and remove VerificationScreen --- .eslintrc.js | 6 +- .prettierrc.js | 9 +- package-lock.json | 21 +- src/App.tsx | 356 ++++++++++-------- src/screens/VerificationScreen.tsx | 557 ----------------------------- 5 files changed, 223 insertions(+), 726 deletions(-) delete mode 100644 src/screens/VerificationScreen.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 7e38b6e..187894b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,2 +1,4 @@ -# Path: OfflineFaceAuth/.eslintrc.js -# Purpose: ESLint configuration extending react-native-community rules enforcing strict TypeScript, no-any, and import ordering. +module.exports = { + root: true, + extends: '@react-native', +}; diff --git a/.prettierrc.js b/.prettierrc.js index 40867e7..2b54074 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,2 +1,7 @@ -# Path: OfflineFaceAuth/.prettierrc.js -# Purpose: Prettier configuration for JS/TS code formatting enforcing single quotes, trailing commas, 100-char print width. +module.exports = { + arrowParens: 'avoid', + bracketSameLine: true, + bracketSpacing: false, + singleQuote: true, + trailingComma: 'all', +}; diff --git a/package-lock.json b/package-lock.json index e36c627..ad3417c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -122,7 +122,6 @@ "node_modules/@aws-sdk/client-s3": { "version": "3.1060.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", @@ -518,7 +517,6 @@ "node_modules/@babel/core": { "version": "7.29.7", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -548,7 +546,6 @@ "version": "7.29.7", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", @@ -2108,7 +2105,6 @@ "node_modules/@babel/preset-env": { "version": "7.29.7", "license": "MIT", - "peer": true, "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", @@ -2608,7 +2604,6 @@ "node_modules/@jest/environment": { "version": "29.7.0", "license": "MIT", - "peer": true, "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", @@ -3537,6 +3532,8 @@ }, "node_modules/@react-native/metro-config": { "version": "0.73.5", + "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.73.5.tgz", + "integrity": "sha512-3bNWoHzOzP/+qoLJtRhOVXrnxKmSY3i4y5PXyMQlIvvOI/GQbXulPpEZxK/yUrf1MmeXHLLFufFbQWlfDEDoxA==", "dev": true, "license": "MIT", "dependencies": { @@ -3919,7 +3916,6 @@ "version": "5.62.0", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -4109,7 +4105,6 @@ "version": "3.4.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chalk": "^4.1.0", "pngjs": "^7.0.0", @@ -4157,7 +4152,6 @@ "node_modules/acorn": { "version": "8.16.0", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4707,7 +4701,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4774,7 +4767,6 @@ "node >=0.10.0" ], "license": "MIT", - "peer": true, "bin": { "bunyan": "bin/bunyan" }, @@ -5525,7 +5517,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@wix-pilot/core": "^3.4.2", "@wix-pilot/detox": "^1.0.13", @@ -6002,7 +5993,6 @@ "version": "8.57.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6527,7 +6517,6 @@ "version": "29.7.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", @@ -7936,7 +7925,6 @@ "version": "29.7.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -10037,7 +10025,6 @@ "version": "2.8.8", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -10193,7 +10180,6 @@ "node_modules/react": { "version": "18.2.0", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10216,7 +10202,6 @@ "node_modules/react-native": { "version": "0.73.6", "license": "MIT", - "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.6.3", "@react-native-community/cli": "12.3.6", @@ -10302,7 +10287,6 @@ "node_modules/react-native-worklets-core": { "version": "0.4.0", "license": "MIT", - "peer": true, "dependencies": { "string-hash-64": "^1.0.3" }, @@ -11701,7 +11685,6 @@ "version": "5.0.4", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/App.tsx b/src/App.tsx index 441a9cc..ed75cb0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,52 +24,19 @@ import { type SmokeTestResult, } from './storage/database/SmokeTest'; -type OfflineFaceAuthResult = { - accepted: boolean; - externalModelProcessed?: boolean; - timestampNs: number; - sharpnessScore: number; - faceMeshProcessed?: boolean; - mobileFaceNetProcessed?: boolean; - droppedFrameCount?: number; - replacedFrameCount?: number; - faceMeshThreadCount?: number; - mobileFaceNetThreadCount?: number; - livenessState?: number; - livenessChallenge?: number; - faceDetected?: boolean; - ear?: number; - mar?: number; - yaw?: number; - pitch?: number; - roll?: number; - framesProcessed?: number; - framesWithFace?: number; - embeddingValid?: boolean; - embeddingFrameId?: number; - embedding: Float32Array; - embeddingPreview?: number[]; - embeddingLength?: number; - embeddingByteLength?: number; -}; +import type { + NativeBridgeModule, + NativeFaceAuthResult, + NativeLivenessChallenge, +} from './types/native'; type OfflineFaceAuthGlobal = { - getLatestResult: () => OfflineFaceAuthResult; + getLatestResult: () => NativeFaceAuthResult; isInitialized: () => boolean; setLivenessState?: (state: number) => boolean; setLivenessChallenge?: (challenge: number) => boolean; }; -type NativeBridgeModule = { - initializeEngine: (modelPath?: string) => Promise; - ensureJsiInstalled: () => Promise; - setLivenessPassed?: (passed: boolean) => Promise; - setLivenessState?: (state: string) => Promise; - setLivenessChallenge?: (challenge: string) => Promise; -}; - -type NativeChallenge = 'NONE' | 'BLINK' | 'SMILE' | 'TURN_LEFT' | 'TURN_RIGHT'; - const MODEL_PATH = '/sdcard/Download/mobilefacenet.tflite'; const LIVENESS_STATE_NAMES = [ 'IDLE', @@ -86,84 +53,168 @@ const LIVENESS_CHALLENGE_NAMES = [ 'TURN_RIGHT', ] as const; +const COLORS = { + background: '#050B1A', + primary: '#00BFFF', + success: '#00FF88', + error: '#FF4D4D', + glass: 'rgba(255, 255, 255, 0.05)', + glassBorder: 'rgba(255, 255, 255, 0.1)', + textPrimary: '#FFFFFF', + textSecondary: '#94a3b8', +}; + const styles = StyleSheet.create({ - root: {flex: 1, backgroundColor: '#0f172a'}, - scrollContent: {padding: 24}, - header: {fontSize: 28, fontWeight: '700', color: '#f8fafc', marginBottom: 8}, - subheader: {fontSize: 15, color: '#cbd5e1', marginBottom: 24}, - card: { - backgroundColor: '#111827', - borderRadius: 16, - padding: 16, - marginBottom: 16, - borderWidth: 1, - borderColor: '#1f2937', - }, - cameraSection: { - marginBottom: 16, + root: {flex: 1, backgroundColor: COLORS.background}, + scrollContent: {paddingHorizontal: 20, paddingBottom: 40}, + headerSection: { + marginTop: 20, + marginBottom: 24, + alignItems: 'center', }, - cameraLabel: { - color: '#e2e8f0', - fontSize: 14, - fontWeight: '700', + logoText: { + fontSize: 12, + fontWeight: '900', + color: COLORS.primary, + letterSpacing: 4, marginBottom: 8, + textTransform: 'uppercase', }, - label: {fontSize: 13, color: '#94a3b8', marginBottom: 6}, - value: {fontSize: 16, color: '#f8fafc', fontWeight: '600'}, - button: { - backgroundColor: '#2563eb', - borderRadius: 14, - paddingVertical: 14, - paddingHorizontal: 18, - alignItems: 'center', - marginBottom: 16, + header: { + fontSize: 24, + fontWeight: '800', + color: COLORS.textPrimary, + textAlign: 'center', }, - secondaryButton: { - backgroundColor: '#0f766e', + subheader: { + fontSize: 14, + color: COLORS.textSecondary, + textAlign: 'center', + marginTop: 4, + lineHeight: 20, }, - passButton: { - backgroundColor: '#7c3aed', + cameraSection: { + marginBottom: 24, + borderRadius: 20, + overflow: 'hidden', + backgroundColor: '#000', + borderWidth: 1, + borderColor: COLORS.glassBorder, + shadowColor: COLORS.primary, + shadowOffset: {width: 0, height: 4}, + shadowOpacity: 0.2, + shadowRadius: 10, + elevation: 5, }, challengeGrid: { flexDirection: 'row', flexWrap: 'wrap', - gap: 8, - marginBottom: 16, + margin: -6, + marginBottom: 18, }, challengeButton: { flexGrow: 1, - flexBasis: '48%', - backgroundColor: '#334155', - borderRadius: 12, - paddingVertical: 12, - paddingHorizontal: 10, + flexBasis: '45%', + backgroundColor: COLORS.glass, + borderRadius: 16, + paddingVertical: 14, alignItems: 'center', + borderWidth: 1, + borderColor: COLORS.glassBorder, + margin: 6, }, challengeButtonText: { - color: '#f8fafc', - fontSize: 13, - fontWeight: '800', + color: COLORS.textPrimary, + fontSize: 14, + fontWeight: '600', + }, + mainActions: { + marginBottom: 12, + }, + button: { + backgroundColor: COLORS.primary, + borderRadius: 16, + paddingVertical: 16, + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center', + shadowColor: COLORS.primary, + shadowOffset: {width: 0, height: 4}, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 4, + marginBottom: 12, + }, + passButton: { + backgroundColor: 'transparent', + borderWidth: 1, + borderColor: COLORS.success, + }, + secondaryButton: { + backgroundColor: 'transparent', + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.2)', }, disabledButton: { - opacity: 0.6, + opacity: 0.5, + }, + buttonText: { + color: COLORS.textPrimary, + fontSize: 16, + fontWeight: '700', + letterSpacing: 0.5, + }, + passButtonText: { + color: COLORS.success, }, - buttonText: {color: '#eff6ff', fontSize: 16, fontWeight: '700'}, console: { - backgroundColor: '#020617', - borderRadius: 16, + backgroundColor: 'rgba(0, 0, 0, 0.4)', + borderRadius: 20, borderWidth: 1, - borderColor: '#1e293b', + borderColor: COLORS.glassBorder, padding: 16, - minHeight: 260, + minHeight: 200, + marginBottom: 24, }, - statusSection: { - marginTop: 16, + consoleHeader: { + fontSize: 11, + fontWeight: '700', + color: COLORS.primary, + marginBottom: 8, + textTransform: 'uppercase', + letterSpacing: 1, }, consoleText: { - color: '#bfdbfe', + color: '#E2E8F0', fontFamily: 'monospace', + fontSize: 12, + lineHeight: 18, + }, + statusSection: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + }, + card: { + backgroundColor: COLORS.glass, + borderRadius: 16, + padding: 12, + borderWidth: 1, + borderColor: COLORS.glassBorder, + width: '48%', + marginBottom: 12, + }, + label: { + fontSize: 10, + color: COLORS.textSecondary, + marginBottom: 4, + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + value: { fontSize: 13, - lineHeight: 19, + color: COLORS.textPrimary, + fontWeight: '700', }, }); @@ -178,13 +229,13 @@ function readGlobalEngine(): OfflineFaceAuthGlobal | undefined { .__offlineFaceAuth; } -function readEmbedding(result: OfflineFaceAuthResult): Float32Array { +function readEmbedding(result: NativeFaceAuthResult): Float32Array { return result.embedding instanceof Float32Array ? result.embedding : new Float32Array(); } -function isUsableEmbedding(result: OfflineFaceAuthResult): boolean { +function isUsableEmbedding(result: NativeFaceAuthResult): boolean { const embedding = readEmbedding(result); return ( (result.embeddingValid === true || result.accepted === true) && @@ -192,11 +243,11 @@ function isUsableEmbedding(result: OfflineFaceAuthResult): boolean { ); } -function formatResult(result: OfflineFaceAuthResult): string { +function formatResult(result: NativeFaceAuthResult): string { const embedding = readEmbedding(result); const embeddingArray = Array.from(embedding); const preview = isUsableEmbedding(result) - ? embeddingArray.slice(0, 16).map((value) => value.toFixed(6)) + ? embeddingArray.slice(0, 16).map(value => value.toFixed(6)) : []; const livenessStateName = LIVENESS_STATE_NAMES[result.livenessState ?? 0] ?? 'UNKNOWN'; @@ -240,7 +291,7 @@ function formatResult(result: OfflineFaceAuthResult): string { } function formatSmokeTestResult(result: SmokeTestResult): string { - const lines = result.steps.map((step) => { + const lines = result.steps.map(step => { const status = step.passed ? 'PASS' : 'FAIL'; return `${status} ${step.name}: ${step.detail}`; }); @@ -261,7 +312,7 @@ function formatStorageSmokeTestResults( '', `MMKV smoke test: ${mmkvResult.passed ? 'PASS' : 'FAIL'}`, `Duration: ${mmkvResult.durationMs}ms`, - ...mmkvResult.steps.map((step) => { + ...mmkvResult.steps.map(step => { const status = step.passed ? 'PASS' : 'FAIL'; return `${status} ${step.name}: ${step.detail}`; }), @@ -275,7 +326,9 @@ export default function App(): React.JSX.Element { useState(false); const [storageTestRunning, setStorageTestRunning] = useState(false); const [previewReady, setPreviewReady] = useState(false); - const [consoleOutput, setConsoleOutput] = useState('Booting verification harness...'); + const [consoleOutput, setConsoleOutput] = useState( + 'Booting verification harness...', + ); useEffect(() => { const unsubscribe = startConnectivityWatcher(); @@ -310,7 +363,9 @@ export default function App(): React.JSX.Element { ); } - const pluginInstalled = await initializeFrameProcessorBridge(MODEL_PATH); + const pluginInstalled = await initializeFrameProcessorBridge( + MODEL_PATH, + ); setFrameProcessorPluginReady(pluginInstalled); const deadline = Date.now() + 3000; @@ -326,7 +381,7 @@ export default function App(): React.JSX.Element { ); return; } - await new Promise((resolve) => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 50)); } const status = refreshStatus(); @@ -414,7 +469,7 @@ export default function App(): React.JSX.Element { }, [refreshStatus]); const handleChallenge = useCallback( - async (challenge: NativeChallenge) => { + async (challenge: NativeLivenessChallenge) => { try { await setNativeLivenessChallenge(challenge); setConsoleOutput( @@ -461,20 +516,26 @@ export default function App(): React.JSX.Element { return ( - + - Offline FaceAuth Harness - - Verifies JNI bootstrap, JSI injection, and zero-copy embedding access. - + contentContainerStyle={styles.scrollContent}> + + Nayan Secure + Offline FaceAuth Harness + + Verifies JNI bootstrap, JSI injection, and zero-copy embedding + access. + + - Front Camera Preview setPreviewReady(true)} /> @@ -483,66 +544,69 @@ export default function App(): React.JSX.Element { handleChallenge('BLINK')} - > + onPress={() => handleChallenge('BLINK')}> Start Blink handleChallenge('TURN_LEFT')} - > + onPress={() => handleChallenge('TURN_LEFT')}> Start Turn Left handleChallenge('TURN_RIGHT')} - > + onPress={() => handleChallenge('TURN_RIGHT')}> Start Turn Right handleChallenge('NONE')} - > - Reset Liveness + onPress={() => handleChallenge('NONE')}> + + Reset Liveness + - - Read Latest Native Result - - - - Mark Liveness Passed - - - - - {storageTestRunning - ? 'Running Storage Smoke Tests' - : 'Run Storage Smoke Tests'} - - + + + Read Latest Native Result + + + + + Mark Liveness Passed + + + + + + {storageTestRunning + ? 'Running Storage Smoke Tests' + : 'Run Storage Smoke Tests'} + + + + System Console {consoleOutput} - {summary.map((item) => ( + {summary.map(item => ( {item.label} - {item.value} + + {item.value} + ))} diff --git a/src/screens/VerificationScreen.tsx b/src/screens/VerificationScreen.tsx deleted file mode 100644 index f64fafa..0000000 --- a/src/screens/VerificationScreen.tsx +++ /dev/null @@ -1,557 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { - View, - Text, - StyleSheet, - Animated, - Easing, - TouchableOpacity, - Dimensions, - SafeAreaView, - StatusBar -} from 'react-native'; -import { - Camera, - useCameraDevice, - useCameraFormat, - useCameraPermission, - useFrameProcessor, -} from 'react-native-vision-camera'; -import { - getLatestFrameResult, - getNativeFrameProcessorPlugin, - setNativeLivenessState -} from '../components/camera/FrameProcessorBridge'; - -const { width, height } = Dimensions.get('window'); - -// --- Design System Tokens --- -const COLORS = { - background: '#050B1A', // Deep Navy - primary: '#00BFFF', // Electric Blue - success: '#00FF88', // Success Green - error: '#FF4D4D', // Error Red - textPrimary: '#FFFFFF', - textSecondary: 'rgba(255, 255, 255, 0.7)', - glassBackground: 'rgba(255, 255, 255, 0.05)', - glassBorder: 'rgba(255, 255, 255, 0.1)', -}; - -const TYPOGRAPHY = { - header: { fontSize: 24, fontWeight: '700' as const, color: COLORS.textPrimary, letterSpacing: 0.5 }, - title: { fontSize: 20, fontWeight: '600' as const, color: COLORS.textPrimary }, - subtitle: { fontSize: 14, fontWeight: '400' as const, color: COLORS.textSecondary }, - instruction: { fontSize: 18, fontWeight: '500' as const, color: COLORS.textPrimary }, - buttonText: { fontSize: 16, fontWeight: '600' as const, color: COLORS.background }, - caption: { fontSize: 12, fontWeight: '400' as const, color: COLORS.textSecondary }, -}; - -const LIVENESS_STATE_CODES = [ - 'IDLE', - 'DETECTED', - 'CHALLENGE_ACTIVE', - 'LIVENESS_PASS', - 'LIVENESS_FAIL', -] as const; - -const LIVENESS_CHALLENGE_CODES = [ - 'NONE', - 'BLINK', - 'SMILE', - 'TURN_LEFT', - 'TURN_RIGHT', -] as const; - -const STAGES = ['Face', 'Blink', 'Head Turn', 'Verify']; - -export default function VerificationScreen() { - const [engineState, setEngineState] = useState<{ - state: typeof LIVENESS_STATE_CODES[number]; - challenge: typeof LIVENESS_CHALLENGE_CODES[number]; - failReason?: string; - }>({ - state: 'IDLE', - challenge: 'NONE' - }); - const [progressIndex, setProgressIndex] = useState(0); - - const scanAnim = useRef(new Animated.Value(0)).current; - const pulseAnim = useRef(new Animated.Value(1)).current; - - // --- Camera Setup --- - const device = useCameraDevice('front'); - const format = useCameraFormat(device, [ - {videoResolution: {width: 640, height: 480}}, - {fps: 30}, - ]); - const {hasPermission, requestPermission} = useCameraPermission(); - const nayanFrameProcessorPlugin = getNativeFrameProcessorPlugin(); - - const frameProcessor = useFrameProcessor((frame) => { - 'worklet'; - nayanFrameProcessorPlugin?.call(frame); - }, [nayanFrameProcessorPlugin]); - - useEffect(() => { - if (!hasPermission) { - requestPermission(); - } - }, [hasPermission, requestPermission]); - - // --- Engine Telemetry Polling --- - useEffect(() => { - if (!hasPermission) return; - - const interval = setInterval(() => { - const latest = getLatestFrameResult(); - if (latest != null) { - const nextState = LIVENESS_STATE_CODES[latest.livenessState ?? 0] ?? 'IDLE'; - const nextChallenge = LIVENESS_CHALLENGE_CODES[latest.livenessChallenge ?? 0] ?? 'NONE'; - - let failReason = undefined; - if (nextState === 'LIVENESS_FAIL') { - if (!latest.passiveTextureOk || !latest.passiveDepthOk) { - failReason = 'Spoof attempt detected'; - } else if (!latest.faceDetected) { - failReason = 'Face not detected during challenge'; - } else { - failReason = 'Challenge failed or timed out'; - } - } - - setEngineState(prev => { - if (prev.state === nextState && prev.challenge === nextChallenge && prev.failReason === failReason) { - return prev; - } - return { state: nextState, challenge: nextChallenge, failReason }; - }); - } - }, 100); - - return () => clearInterval(interval); - }, [hasPermission]); - - // --- Map Progress --- - useEffect(() => { - const { state, challenge } = engineState; - if (state === 'IDLE') { - setProgressIndex(0); - } else if (state === 'DETECTED' && progressIndex === 0) { - setProgressIndex(0); - } else if (state === 'CHALLENGE_ACTIVE' && challenge === 'BLINK') { - setProgressIndex(1); - } else if (state === 'CHALLENGE_ACTIVE' && (challenge === 'TURN_LEFT' || challenge === 'TURN_RIGHT' || challenge === 'SMILE')) { - setProgressIndex(2); - } else if (state === 'DETECTED' && progressIndex >= 2) { - setProgressIndex(3); - } else if (state === 'LIVENESS_PASS') { - setProgressIndex(4); - } - }, [engineState, progressIndex]); - - // --- Visual Animations --- - useEffect(() => { - Animated.loop( - Animated.sequence([ - Animated.timing(pulseAnim, { - toValue: 1.05, - duration: 1200, - useNativeDriver: true, - easing: Easing.inOut(Easing.ease), - }), - Animated.timing(pulseAnim, { - toValue: 1, - duration: 1200, - useNativeDriver: true, - easing: Easing.inOut(Easing.ease), - }) - ]) - ).start(); - - Animated.loop( - Animated.sequence([ - Animated.timing(scanAnim, { - toValue: 1, - duration: 2500, - useNativeDriver: true, - easing: Easing.inOut(Easing.ease), - }), - Animated.timing(scanAnim, { - toValue: 0, - duration: 2500, - useNativeDriver: true, - easing: Easing.inOut(Easing.ease), - }) - ]) - ).start(); - }, [pulseAnim, scanAnim]); - - const handleRetry = async () => { - try { - await setNativeLivenessState('IDLE'); - } catch (e) { - // Ignore if native bridge fails - } - setEngineState({ state: 'IDLE', challenge: 'NONE' }); - setProgressIndex(0); - }; - - const scanTranslateY = scanAnim.interpolate({ - inputRange: [0, 1], - outputRange: [0, height * 0.55], - }); - - const { state, challenge, failReason } = engineState; - - // --- Real-Time Instruction Mapping --- - let instruction = "Position your face in frame"; - if (state === 'IDLE') { - instruction = "Position your face in frame"; - } else if (state === 'DETECTED') { - if (progressIndex >= 2) instruction = "Verifying liveness..."; - else instruction = "Face detected"; - } else if (state === 'CHALLENGE_ACTIVE') { - if (challenge === 'BLINK') instruction = "Blink once"; - else if (challenge === 'TURN_LEFT' || challenge === 'TURN_RIGHT') instruction = "Turn your head"; - else if (challenge === 'SMILE') instruction = "Smile"; - else instruction = "Hold still"; - } - - const isOverlayVisible = state === 'LIVENESS_PASS' || state === 'LIVENESS_FAIL'; - const isPass = state === 'LIVENESS_PASS'; - - return ( - - - - {/* Header Area */} - - NAYAN - Liveness Verification - AI-Powered Face Authentication - - - {/* Main Camera Section */} - - {hasPermission && device != null ? ( - - ) : ( - - )} - - {/* Bounding Guide & Scanning Target */} - - - - - - - - - - - - - {/* Animated Scanning Laser Line */} - - - {/* Glassmorphic Live Challenge Panel at Bottom of Camera */} - {!isOverlayVisible && ( - - {instruction} - - - - {STAGES.map((stage, index) => { - const isCompleted = index < progressIndex; - const isActive = index === progressIndex; - const isLast = index === STAGES.length - 1; - - return ( - - - {!isLast && ( - - )} - - ); - })} - - - {STAGES.map((stage, index) => ( - - {stage} - - ))} - - - - )} - - - {/* Full Screen Pass / Fail Overlay */} - {isOverlayVisible && ( - - - {isPass ? '✅' : '❌'} - - {isPass ? 'Liveness Verification Passed' : 'Liveness Verification Failed'} - - - - {isPass - ? 'Identity confirmed as a live human.' - : (failReason ?? 'Possible reasons:\n• Face not detected\n• Blink challenge failed\n• Head-turn challenge failed\n• Spoof attempt detected') - } - - - - {isPass ? 'Verify Again' : 'Retry'} - - - - )} - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: COLORS.background, - }, - header: { - paddingTop: 20, - paddingHorizontal: 24, - paddingBottom: 16, - alignItems: 'center', - zIndex: 10, - }, - logo: { - fontSize: 22, - fontWeight: '900', - color: COLORS.primary, - letterSpacing: 6, - marginBottom: 6, - }, - title: { - ...TYPOGRAPHY.header, - marginBottom: 4, - }, - subtitle: { - ...TYPOGRAPHY.subtitle, - }, - cameraSection: { - flex: 1, - marginHorizontal: 16, - marginBottom: 24, - borderRadius: 32, - overflow: 'hidden', - position: 'relative', - backgroundColor: '#0a1128', - borderWidth: 2, - borderColor: 'rgba(0, 191, 255, 0.2)', - }, - mockCameraFeed: { - ...StyleSheet.absoluteFillObject, - backgroundColor: '#0A101F', - opacity: 0.8, - }, - boundingGuide: { - position: 'absolute', - top: '12%', - left: '15%', - right: '15%', - bottom: '30%', - justifyContent: 'center', - alignItems: 'center', - }, - corner: { - position: 'absolute', - width: 35, - height: 35, - borderColor: COLORS.primary, - }, - topLeft: { top: 0, left: 0, borderTopWidth: 3, borderLeftWidth: 3, borderTopLeftRadius: 20 }, - topRight: { top: 0, right: 0, borderTopWidth: 3, borderRightWidth: 3, borderTopRightRadius: 20 }, - bottomLeft: { bottom: 0, left: 0, borderBottomWidth: 3, borderLeftWidth: 3, borderBottomLeftRadius: 20 }, - bottomRight: { bottom: 0, right: 0, borderBottomWidth: 3, borderRightWidth: 3, borderBottomRightRadius: 20 }, - faceMeshRing: { - width: 180, - height: 240, - borderRadius: 90, - borderWidth: 1, - borderColor: 'rgba(0, 191, 255, 0.3)', - borderStyle: 'dashed', - justifyContent: 'center', - alignItems: 'center', - }, - innerRing: { - width: 140, - height: 190, - borderRadius: 70, - borderWidth: 1, - borderColor: 'rgba(0, 191, 255, 0.15)', - }, - coreRing: { - position: 'absolute', - width: 80, - height: 110, - borderRadius: 40, - borderWidth: 1, - borderColor: 'rgba(0, 191, 255, 0.1)', - }, - scanLine: { - position: 'absolute', - left: 0, - right: 0, - top: 20, - height: 3, - backgroundColor: COLORS.primary, - shadowColor: COLORS.primary, - shadowOffset: { width: 0, height: 0 }, - shadowOpacity: 1, - shadowRadius: 15, - elevation: 8, - }, - glassPanel: { - position: 'absolute', - bottom: 24, - left: 20, - right: 20, - backgroundColor: COLORS.glassBackground, - borderRadius: 24, - padding: 24, - borderWidth: 1, - borderColor: COLORS.glassBorder, - alignItems: 'center', - }, - instructionText: { - ...TYPOGRAPHY.instruction, - textAlign: 'center', - marginBottom: 20, - }, - progressContainer: { - width: '100%', - }, - progressLineContainer: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: 10, - paddingHorizontal: 5, - }, - progressDot: { - width: 14, - height: 14, - borderRadius: 7, - zIndex: 2, - }, - progressDotCompleted: { backgroundColor: COLORS.success }, - progressDotActive: { backgroundColor: COLORS.primary }, - progressDotPending: { backgroundColor: 'rgba(255, 255, 255, 0.2)' }, - progressLine: { - flex: 1, - height: 2, - marginHorizontal: -2, - zIndex: 1, - }, - progressLineCompleted: { backgroundColor: COLORS.success }, - progressLinePending: { backgroundColor: 'rgba(255, 255, 255, 0.1)' }, - progressLabelsContainer: { - flexDirection: 'row', - justifyContent: 'space-between', - }, - progressLabel: { - ...TYPOGRAPHY.caption, - width: 65, - textAlign: 'center', - }, - progressLabelCompleted: { color: COLORS.success, fontWeight: '500' }, - progressLabelActive: { color: COLORS.primary, fontWeight: '700' }, - progressLabelPending: { color: COLORS.textSecondary }, - glowEffectPrimary: { - shadowColor: COLORS.primary, - shadowOffset: { width: 0, height: 0 }, - shadowOpacity: 1, - shadowRadius: 12, - elevation: 6, - }, - glowEffectSuccess: { - shadowColor: COLORS.success, - shadowOffset: { width: 0, height: 0 }, - shadowOpacity: 1, - shadowRadius: 12, - elevation: 6, - }, - overlay: { - ...StyleSheet.absoluteFillObject, - justifyContent: 'center', - alignItems: 'center', - padding: 24, - zIndex: 100, - }, - overlayPass: { backgroundColor: 'rgba(5, 11, 26, 0.96)' }, - overlayFail: { backgroundColor: 'rgba(5, 11, 26, 0.96)' }, - overlayContent: { - alignItems: 'center', - backgroundColor: COLORS.glassBackground, - padding: 32, - borderRadius: 28, - borderWidth: 1, - borderColor: 'rgba(255,255,255,0.15)', - width: '100%', - }, - overlayIcon: { - fontSize: 72, - marginBottom: 24, - }, - overlayTitle: { - ...TYPOGRAPHY.header, - textAlign: 'center', - marginBottom: 16, - color: COLORS.textPrimary, - }, - overlaySubtitle: { - ...TYPOGRAPHY.subtitle, - textAlign: 'center', - marginBottom: 32, - lineHeight: 24, - }, - button: { - width: '100%', - paddingVertical: 18, - borderRadius: 16, - alignItems: 'center', - }, - buttonPass: { backgroundColor: COLORS.success }, - buttonFail: { backgroundColor: COLORS.error }, - buttonText: { - ...TYPOGRAPHY.buttonText, - fontSize: 18, - }, -});