@hexar/biometric-identity-sdk-react-native 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"BiometricIdentityFlow.d.ts","sourceRoot":"","sources":["../../src/components/BiometricIdentityFlow.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA2C,MAAM,OAAO,CAAC;AAChE,OAAO,EAOL,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,cAAc,EAId,iBAAiB,EAClB,MAAM,oCAAoC,CAAC;AAU5C,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACzD,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACzC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAuStE,CAAC;AAiOF,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"BiometricIdentityFlow.d.ts","sourceRoot":"","sources":["../../src/components/BiometricIdentityFlow.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA2C,MAAM,OAAO,CAAC;AAChE,OAAO,EAOL,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,cAAc,EAId,iBAAiB,EAClB,MAAM,oCAAoC,CAAC;AAU5C,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACzD,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACzC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAwStE,CAAC;AAiOF,eAAe,qBAAqB,CAAC"}
@@ -55,7 +55,8 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
55
55
  const [showInstructions, setShowInstructions] = (0, react_1.useState)(true);
56
56
  const [currentChallenges, setCurrentChallenges] = (0, react_1.useState)([]);
57
57
  const [isLoadingChallenges, setIsLoadingChallenges] = (0, react_1.useState)(false);
58
- // Override SDK language only if explicitly provided
58
+ // Set language early, before any components render
59
+ // Run on mount and whenever language prop changes
59
60
  (0, react_1.useEffect)(() => {
60
61
  if (language) {
61
62
  (0, biometric_identity_sdk_core_1.setLanguage)(language);
@@ -1 +1 @@
1
- {"version":3,"file":"CameraCapture.d.ts","sourceRoot":"","sources":["../../src/components/CameraCapture.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA2B,MAAM,OAAO,CAAC;AAShD,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAIjE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA6FtD,CAAC;AA6IF,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"CameraCapture.d.ts","sourceRoot":"","sources":["../../src/components/CameraCapture.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAsC,MAAM,OAAO,CAAC;AAY3D,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAIjE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAiNtD,CAAC;AA0JF,eAAe,aAAa,CAAC"}
@@ -40,35 +40,123 @@ Object.defineProperty(exports, "__esModule", { value: true });
40
40
  exports.CameraCapture = void 0;
41
41
  const react_1 = __importStar(require("react"));
42
42
  const react_native_1 = require("react-native");
43
+ const react_native_vision_camera_1 = require("react-native-vision-camera");
44
+ const react_native_permissions_1 = require("react-native-permissions");
43
45
  const { width, height } = react_native_1.Dimensions.get('window');
44
46
  const CameraCapture = ({ mode, theme, onCapture, onCancel, }) => {
45
47
  const [isCapturing, setIsCapturing] = (0, react_1.useState)(false);
48
+ const [hasPermission, setHasPermission] = (0, react_1.useState)(false);
46
49
  const cameraRef = (0, react_1.useRef)(null);
50
+ const { hasPermission: cameraPermission, requestPermission } = (0, react_native_vision_camera_1.useCameraPermission)();
51
+ // Get camera device (back camera for document capture)
52
+ const device = (0, react_native_vision_camera_1.useCameraDevice)('back');
53
+ (0, react_1.useEffect)(() => {
54
+ checkPermissions();
55
+ }, []);
56
+ const checkPermissions = async () => {
57
+ try {
58
+ // First check if we already have permission from the hook
59
+ if (cameraPermission) {
60
+ setHasPermission(true);
61
+ return;
62
+ }
63
+ // Otherwise, request permission using the hook
64
+ const granted = await requestPermission();
65
+ setHasPermission(granted);
66
+ // If hook method didn't work, try platform-specific request
67
+ if (!granted) {
68
+ if (react_native_1.Platform.OS === 'ios') {
69
+ const result = await (0, react_native_permissions_1.request)(react_native_permissions_1.PERMISSIONS.IOS.CAMERA);
70
+ setHasPermission(result === react_native_permissions_1.RESULTS.GRANTED);
71
+ }
72
+ else {
73
+ const result = await (0, react_native_permissions_1.request)(react_native_permissions_1.PERMISSIONS.ANDROID.CAMERA);
74
+ setHasPermission(result === react_native_permissions_1.RESULTS.GRANTED);
75
+ }
76
+ }
77
+ }
78
+ catch (error) {
79
+ console.error('Permission check error:', error);
80
+ setHasPermission(false);
81
+ react_native_1.Alert.alert('Camera Permission Required', 'Please enable camera access in your device settings to capture your ID document.');
82
+ }
83
+ };
47
84
  const handleCapture = async () => {
48
- if (isCapturing)
85
+ if (isCapturing || !cameraRef.current || !device)
49
86
  return;
50
87
  try {
51
88
  setIsCapturing(true);
52
- // In real implementation, this would use react-native-vision-camera
53
- // For now, simulate capture
54
- await new Promise(resolve => setTimeout(resolve, 500));
55
- // Mock captured image data
56
- const mockImageData = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...';
57
- onCapture(mockImageData);
89
+ // Capture photo using react-native-vision-camera
90
+ const photo = await cameraRef.current.takePhoto({
91
+ flash: 'off',
92
+ });
93
+ // Convert photo file to base64
94
+ // Try to use react-native-fs if available, otherwise use file path
95
+ try {
96
+ // Try dynamic import of react-native-fs
97
+ const RNFS = require('react-native-fs');
98
+ const base64 = await RNFS.readFile(photo.path, 'base64');
99
+ onCapture(base64);
100
+ }
101
+ catch (fsError) {
102
+ // If react-native-fs is not available, try alternative method
103
+ try {
104
+ // Use fetch with file:// protocol (works on some platforms)
105
+ const fileUri = react_native_1.Platform.OS === 'android' ? `file://${photo.path}` : photo.path;
106
+ const response = await fetch(fileUri);
107
+ const blob = await response.blob();
108
+ // Convert blob to base64 using a workaround
109
+ const reader = new FileReader();
110
+ reader.onloadend = () => {
111
+ const base64data = reader.result;
112
+ const base64 = base64data.includes(',')
113
+ ? base64data.split(',')[1]
114
+ : base64data;
115
+ onCapture(base64);
116
+ setIsCapturing(false);
117
+ };
118
+ reader.onerror = () => {
119
+ throw new Error('Failed to read photo file');
120
+ };
121
+ reader.readAsDataURL(blob);
122
+ return; // Don't set isCapturing to false here, wait for reader
123
+ }
124
+ catch (fetchError) {
125
+ // Final fallback: pass file path (SDK should handle file paths)
126
+ console.warn('Could not convert to base64, using file path:', photo.path);
127
+ onCapture(photo.path);
128
+ }
129
+ }
130
+ setIsCapturing(false);
58
131
  }
59
132
  catch (error) {
60
133
  console.error('Capture error:', error);
134
+ react_native_1.Alert.alert('Capture Failed', 'Could not capture photo. Please try again.');
61
135
  setIsCapturing(false);
62
136
  }
63
137
  };
64
138
  const instructions = mode === 'front'
65
139
  ? 'Position the front of your ID within the frame'
66
140
  : 'Position the back of your ID within the frame';
141
+ if (!hasPermission) {
142
+ return (react_1.default.createElement(react_native_1.View, { style: styles.container },
143
+ react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
144
+ react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, "Camera permission is required"),
145
+ react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.button, { backgroundColor: theme?.primaryColor || '#6366F1' }], onPress: checkPermissions },
146
+ react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, "Grant Permission")),
147
+ react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.button, styles.cancelButton], onPress: onCancel },
148
+ react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, "Cancel")))));
149
+ }
150
+ if (!device) {
151
+ return (react_1.default.createElement(react_native_1.View, { style: styles.container },
152
+ react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
153
+ react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, "Camera not available"),
154
+ react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.button, styles.cancelButton], onPress: onCancel },
155
+ react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, "Cancel")))));
156
+ }
67
157
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
68
158
  react_1.default.createElement(react_native_1.View, { style: styles.cameraContainer },
69
- react_1.default.createElement(react_native_1.View, { style: styles.mockCamera },
70
- react_1.default.createElement(react_native_1.Text, { style: styles.mockCameraText }, "Camera View"),
71
- react_1.default.createElement(react_native_1.Text, { style: styles.mockCameraSubtext }, "(In production, this uses react-native-vision-camera)")),
159
+ react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: cameraRef, style: react_native_1.StyleSheet.absoluteFill, device: device, isActive: true, photo: true }),
72
160
  react_1.default.createElement(react_native_1.View, { style: styles.overlay },
73
161
  react_1.default.createElement(react_native_1.View, { style: styles.frameContainer },
74
162
  react_1.default.createElement(react_native_1.View, { style: styles.frame },
@@ -234,5 +322,18 @@ const styles = react_native_1.StyleSheet.create({
234
322
  buttonPlaceholder: {
235
323
  width: 80,
236
324
  },
325
+ permissionContainer: {
326
+ flex: 1,
327
+ justifyContent: 'center',
328
+ alignItems: 'center',
329
+ padding: 32,
330
+ backgroundColor: '#000000',
331
+ },
332
+ permissionText: {
333
+ color: '#FFFFFF',
334
+ fontSize: 18,
335
+ textAlign: 'center',
336
+ marginBottom: 24,
337
+ },
237
338
  });
238
339
  exports.default = exports.CameraCapture;
@@ -1 +1 @@
1
- {"version":3,"file":"InstructionsScreen.d.ts","sourceRoot":"","sources":["../../src/components/InstructionsScreen.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAoB,MAAM,OAAO,CAAC;AACzC,OAAO,EAML,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAE7G,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAoFhE,CAAC;AAuLF,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"InstructionsScreen.d.ts","sourceRoot":"","sources":["../../src/components/InstructionsScreen.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AACnD,OAAO,EAML,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAE7G,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CA4FhE,CAAC;AAuLF,eAAe,kBAAkB,CAAC"}
@@ -42,13 +42,21 @@ const react_1 = __importStar(require("react"));
42
42
  const react_native_1 = require("react-native");
43
43
  const biometric_identity_sdk_core_1 = require("@hexar/biometric-identity-sdk-core");
44
44
  const InstructionsScreen = ({ theme, language = 'en', onStart, styles: customStyles, }) => {
45
- // Set language when prop changes
45
+ const [strings, setStrings] = (0, react_1.useState)(() => {
46
+ // Set initial language
47
+ if (language) {
48
+ (0, biometric_identity_sdk_core_1.setLanguage)(language);
49
+ }
50
+ return (0, biometric_identity_sdk_core_1.getStrings)();
51
+ });
52
+ // Update strings when language changes
46
53
  (0, react_1.useEffect)(() => {
47
54
  if (language) {
48
55
  (0, biometric_identity_sdk_core_1.setLanguage)(language);
56
+ // Force re-render by updating strings state
57
+ setStrings((0, biometric_identity_sdk_core_1.getStrings)());
49
58
  }
50
59
  }, [language]);
51
- const strings = (0, biometric_identity_sdk_core_1.getStrings)();
52
60
  // Get tips and privacy message based on language
53
61
  const tipsContent = getTipsContent(language);
54
62
  const privacyContent = getPrivacyContent(language);
@@ -1 +1 @@
1
- {"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AASxE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAGtF,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACrC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,UAAU,EAAE,CAAC,SAAS,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACtD,iCAAiC;IACjC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAgDD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAsdtD,CAAC;AA+NF,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAaxE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAGtF,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACrC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,UAAU,EAAE,CAAC,SAAS,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACtD,iCAAiC;IACjC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAgDD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA2kBtD,CAAC;AA4OF,eAAe,aAAa,CAAC"}
@@ -40,6 +40,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
40
40
  exports.VideoRecorder = void 0;
41
41
  const react_1 = __importStar(require("react"));
42
42
  const react_native_1 = require("react-native");
43
+ const react_native_vision_camera_1 = require("react-native-vision-camera");
44
+ const react_native_permissions_1 = require("react-native-permissions");
43
45
  // Default challenge set (used if backend not available)
44
46
  const DEFAULT_CHALLENGES = [
45
47
  {
@@ -94,6 +96,11 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
94
96
  const [overallProgress, setOverallProgress] = (0, react_1.useState)(0);
95
97
  const [completedChallenges, setCompletedChallenges] = (0, react_1.useState)([]);
96
98
  const [frames, setFrames] = (0, react_1.useState)([]);
99
+ const [hasPermission, setHasPermission] = (0, react_1.useState)(false);
100
+ // Camera
101
+ const cameraRef = (0, react_1.useRef)(null);
102
+ const { hasPermission: cameraPermission, requestPermission } = (0, react_native_vision_camera_1.useCameraPermission)();
103
+ const device = (0, react_native_vision_camera_1.useCameraDevice)('front');
97
104
  // Animations
98
105
  const fadeAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
99
106
  const scaleAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(1)).current;
@@ -103,8 +110,41 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
103
110
  // Refs
104
111
  const recordingStartTime = (0, react_1.useRef)(0);
105
112
  const frameInterval = (0, react_1.useRef)(null);
113
+ const frameCaptureInterval = (0, react_1.useRef)(null);
106
114
  // Calculate total duration from challenges
107
115
  const totalDuration = duration || challenges.reduce((sum, c) => sum + c.duration_ms, 0) + 2000;
116
+ // Check camera permissions
117
+ (0, react_1.useEffect)(() => {
118
+ const checkPermissions = async () => {
119
+ try {
120
+ // First check if we already have permission from the hook
121
+ if (cameraPermission) {
122
+ setHasPermission(true);
123
+ return;
124
+ }
125
+ // Otherwise, request permission using the hook
126
+ const granted = await requestPermission();
127
+ setHasPermission(granted);
128
+ // If hook method didn't work, try platform-specific request
129
+ if (!granted) {
130
+ if (react_native_1.Platform.OS === 'ios') {
131
+ const result = await (0, react_native_permissions_1.request)(react_native_permissions_1.PERMISSIONS.IOS.CAMERA);
132
+ setHasPermission(result === react_native_permissions_1.RESULTS.GRANTED);
133
+ }
134
+ else {
135
+ const result = await (0, react_native_permissions_1.request)(react_native_permissions_1.PERMISSIONS.ANDROID.CAMERA);
136
+ setHasPermission(result === react_native_permissions_1.RESULTS.GRANTED);
137
+ }
138
+ }
139
+ }
140
+ catch (error) {
141
+ console.error('Permission check error:', error);
142
+ setHasPermission(false);
143
+ react_native_1.Alert.alert('Camera Permission Required', 'Please enable camera access in your device settings to record your face video.');
144
+ }
145
+ };
146
+ checkPermissions();
147
+ }, [cameraPermission, requestPermission]);
108
148
  // Initialize challenges
109
149
  (0, react_1.useEffect)(() => {
110
150
  const initChallenges = async () => {
@@ -215,16 +255,47 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
215
255
  ])).start();
216
256
  }, [arrowAnim]);
217
257
  // Start recording
218
- const startRecording = (0, react_1.useCallback)(() => {
258
+ const startRecording = (0, react_1.useCallback)(async () => {
219
259
  setPhase('recording');
220
260
  recordingStartTime.current = Date.now();
221
- // Simulate frame capture (in real implementation, this would capture actual camera frames)
222
- frameInterval.current = setInterval(() => {
223
- setFrames(prev => [...prev, `frame_${Date.now()}`]);
224
- }, 100); // Capture ~10 FPS
261
+ // Start capturing frames from camera
262
+ if (cameraRef.current && device) {
263
+ // Capture frames periodically (every 100ms = ~10 FPS)
264
+ frameCaptureInterval.current = setInterval(async () => {
265
+ try {
266
+ // Take a photo frame (react-native-vision-camera doesn't have direct frame access in v4)
267
+ // We'll use takeSnapshot or capture frames via photo
268
+ const photo = await cameraRef.current?.takePhoto({
269
+ flash: 'off',
270
+ });
271
+ if (photo) {
272
+ // Convert to base64 if possible, otherwise use file path
273
+ try {
274
+ const RNFS = require('react-native-fs');
275
+ const base64 = await RNFS.readFile(photo.path, 'base64');
276
+ setFrames(prev => [...prev, base64]);
277
+ }
278
+ catch (fsError) {
279
+ // Fallback: use file path
280
+ setFrames(prev => [...prev, photo.path]);
281
+ }
282
+ }
283
+ }
284
+ catch (error) {
285
+ console.warn('Frame capture error:', error);
286
+ // Continue even if one frame fails
287
+ }
288
+ }, 100);
289
+ }
290
+ else {
291
+ // Fallback: simulate frames if camera not available
292
+ frameInterval.current = setInterval(() => {
293
+ setFrames(prev => [...prev, `frame_${Date.now()}`]);
294
+ }, 100);
295
+ }
225
296
  // Start first challenge
226
297
  runChallenge(0);
227
- }, []);
298
+ }, [device]);
228
299
  // Run a specific challenge
229
300
  const runChallenge = (0, react_1.useCallback)((index) => {
230
301
  if (index >= challenges.length) {
@@ -283,15 +354,18 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
283
354
  if (frameInterval.current) {
284
355
  clearInterval(frameInterval.current);
285
356
  }
357
+ if (frameCaptureInterval.current) {
358
+ clearInterval(frameCaptureInterval.current);
359
+ }
286
360
  setPhase('processing');
287
361
  setOverallProgress(100);
288
- // Simulate processing delay
362
+ // Process and return result
289
363
  setTimeout(() => {
290
364
  const result = {
291
365
  frames,
292
366
  duration: Date.now() - recordingStartTime.current,
293
367
  instructionsFollowed: completedChallenges.length === challenges.length,
294
- qualityScore: 85 + Math.random() * 10, // Simulated quality score
368
+ qualityScore: frames.length > 0 ? 85 + Math.random() * 10 : 0, // Quality based on frames captured
295
369
  challengesCompleted: completedChallenges,
296
370
  sessionId,
297
371
  };
@@ -331,10 +405,23 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
331
405
  }
332
406
  return null;
333
407
  };
408
+ if (!hasPermission) {
409
+ return (react_1.default.createElement(react_native_1.View, { style: styles.container },
410
+ react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
411
+ react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, "Camera permission is required"),
412
+ react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }], onPress: onCancel },
413
+ react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, "Cancel")))));
414
+ }
415
+ if (!device) {
416
+ return (react_1.default.createElement(react_native_1.View, { style: styles.container },
417
+ react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
418
+ react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, "Camera not available"),
419
+ react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }], onPress: onCancel },
420
+ react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, "Cancel")))));
421
+ }
334
422
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
335
423
  react_1.default.createElement(react_native_1.View, { style: styles.cameraContainer },
336
- react_1.default.createElement(react_native_1.View, { style: styles.mockCamera },
337
- react_1.default.createElement(react_native_1.Text, { style: styles.mockCameraText }, "Front Camera")),
424
+ react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: cameraRef, style: react_native_1.StyleSheet.absoluteFill, device: device, isActive: phase === 'recording' || phase === 'countdown', video: false, photo: true }),
338
425
  react_1.default.createElement(react_native_1.View, { style: styles.overlay },
339
426
  react_1.default.createElement(react_native_1.View, { style: [
340
427
  styles.faceOval,
@@ -604,6 +691,19 @@ const styles = react_native_1.StyleSheet.create({
604
691
  fontSize: 20,
605
692
  fontWeight: '600',
606
693
  },
694
+ permissionContainer: {
695
+ flex: 1,
696
+ justifyContent: 'center',
697
+ alignItems: 'center',
698
+ padding: 32,
699
+ backgroundColor: '#000000',
700
+ },
701
+ permissionText: {
702
+ color: '#FFFFFF',
703
+ fontSize: 18,
704
+ textAlign: 'center',
705
+ marginBottom: 24,
706
+ },
607
707
  bottomContainer: {
608
708
  paddingVertical: 32,
609
709
  paddingHorizontal: 24,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexar/biometric-identity-sdk-react-native",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "React Native wrapper for Biometric Identity SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -73,7 +73,8 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
73
73
  const [currentChallenges, setCurrentChallenges] = useState<ChallengeAction[]>([]);
74
74
  const [isLoadingChallenges, setIsLoadingChallenges] = useState(false);
75
75
 
76
- // Override SDK language only if explicitly provided
76
+ // Set language early, before any components render
77
+ // Run on mount and whenever language prop changes
77
78
  useEffect(() => {
78
79
  if (language) {
79
80
  setLanguage(language);
@@ -3,7 +3,7 @@
3
3
  * Handles ID document photo capture
4
4
  */
5
5
 
6
- import React, { useState, useRef } from 'react';
6
+ import React, { useState, useRef, useEffect } from 'react';
7
7
  import {
8
8
  View,
9
9
  Text,
@@ -11,7 +11,10 @@ import {
11
11
  TouchableOpacity,
12
12
  Dimensions,
13
13
  Platform,
14
+ Alert,
14
15
  } from 'react-native';
16
+ import { Camera, useCameraDevice, useCameraPermission } from 'react-native-vision-camera';
17
+ import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
15
18
  import { ThemeConfig } from '@hexar/biometric-identity-sdk-core';
16
19
 
17
20
  const { width, height } = Dimensions.get('window');
@@ -30,24 +33,101 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
30
33
  onCancel,
31
34
  }) => {
32
35
  const [isCapturing, setIsCapturing] = useState(false);
33
- const cameraRef = useRef<any>(null);
36
+ const [hasPermission, setHasPermission] = useState(false);
37
+ const cameraRef = useRef<Camera>(null);
38
+ const { hasPermission: cameraPermission, requestPermission } = useCameraPermission();
39
+
40
+ // Get camera device (back camera for document capture)
41
+ const device = useCameraDevice('back');
42
+
43
+ useEffect(() => {
44
+ checkPermissions();
45
+ }, []);
46
+
47
+ const checkPermissions = async () => {
48
+ try {
49
+ // First check if we already have permission from the hook
50
+ if (cameraPermission) {
51
+ setHasPermission(true);
52
+ return;
53
+ }
54
+
55
+ // Otherwise, request permission using the hook
56
+ const granted = await requestPermission();
57
+ setHasPermission(granted);
58
+
59
+ // If hook method didn't work, try platform-specific request
60
+ if (!granted) {
61
+ if (Platform.OS === 'ios') {
62
+ const result = await request(PERMISSIONS.IOS.CAMERA);
63
+ setHasPermission(result === RESULTS.GRANTED);
64
+ } else {
65
+ const result = await request(PERMISSIONS.ANDROID.CAMERA);
66
+ setHasPermission(result === RESULTS.GRANTED);
67
+ }
68
+ }
69
+ } catch (error) {
70
+ console.error('Permission check error:', error);
71
+ setHasPermission(false);
72
+ Alert.alert(
73
+ 'Camera Permission Required',
74
+ 'Please enable camera access in your device settings to capture your ID document.'
75
+ );
76
+ }
77
+ };
34
78
 
35
79
  const handleCapture = async () => {
36
- if (isCapturing) return;
80
+ if (isCapturing || !cameraRef.current || !device) return;
37
81
 
38
82
  try {
39
83
  setIsCapturing(true);
40
84
 
41
- // In real implementation, this would use react-native-vision-camera
42
- // For now, simulate capture
43
- await new Promise(resolve => setTimeout(resolve, 500));
85
+ // Capture photo using react-native-vision-camera
86
+ const photo = await cameraRef.current.takePhoto({
87
+ flash: 'off',
88
+ });
44
89
 
45
- // Mock captured image data
46
- const mockImageData = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...';
90
+ // Convert photo file to base64
91
+ // Try to use react-native-fs if available, otherwise use file path
92
+ try {
93
+ // Try dynamic import of react-native-fs
94
+ const RNFS = require('react-native-fs');
95
+ const base64 = await RNFS.readFile(photo.path, 'base64');
96
+ onCapture(base64);
97
+ } catch (fsError) {
98
+ // If react-native-fs is not available, try alternative method
99
+ try {
100
+ // Use fetch with file:// protocol (works on some platforms)
101
+ const fileUri = Platform.OS === 'android' ? `file://${photo.path}` : photo.path;
102
+ const response = await fetch(fileUri);
103
+ const blob = await response.blob();
104
+
105
+ // Convert blob to base64 using a workaround
106
+ const reader = new FileReader();
107
+ reader.onloadend = () => {
108
+ const base64data = reader.result as string;
109
+ const base64 = base64data.includes(',')
110
+ ? base64data.split(',')[1]
111
+ : base64data;
112
+ onCapture(base64);
113
+ setIsCapturing(false);
114
+ };
115
+ reader.onerror = () => {
116
+ throw new Error('Failed to read photo file');
117
+ };
118
+ reader.readAsDataURL(blob);
119
+ return; // Don't set isCapturing to false here, wait for reader
120
+ } catch (fetchError) {
121
+ // Final fallback: pass file path (SDK should handle file paths)
122
+ console.warn('Could not convert to base64, using file path:', photo.path);
123
+ onCapture(photo.path);
124
+ }
125
+ }
47
126
 
48
- onCapture(mockImageData);
127
+ setIsCapturing(false);
49
128
  } catch (error) {
50
129
  console.error('Capture error:', error);
130
+ Alert.alert('Capture Failed', 'Could not capture photo. Please try again.');
51
131
  setIsCapturing(false);
52
132
  }
53
133
  };
@@ -56,16 +136,55 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
56
136
  ? 'Position the front of your ID within the frame'
57
137
  : 'Position the back of your ID within the frame';
58
138
 
139
+ if (!hasPermission) {
140
+ return (
141
+ <View style={styles.container}>
142
+ <View style={styles.permissionContainer}>
143
+ <Text style={styles.permissionText}>Camera permission is required</Text>
144
+ <TouchableOpacity
145
+ style={[styles.button, { backgroundColor: theme?.primaryColor || '#6366F1' }]}
146
+ onPress={checkPermissions}
147
+ >
148
+ <Text style={styles.buttonText}>Grant Permission</Text>
149
+ </TouchableOpacity>
150
+ <TouchableOpacity
151
+ style={[styles.button, styles.cancelButton]}
152
+ onPress={onCancel}
153
+ >
154
+ <Text style={styles.buttonText}>Cancel</Text>
155
+ </TouchableOpacity>
156
+ </View>
157
+ </View>
158
+ );
159
+ }
160
+
161
+ if (!device) {
162
+ return (
163
+ <View style={styles.container}>
164
+ <View style={styles.permissionContainer}>
165
+ <Text style={styles.permissionText}>Camera not available</Text>
166
+ <TouchableOpacity
167
+ style={[styles.button, styles.cancelButton]}
168
+ onPress={onCancel}
169
+ >
170
+ <Text style={styles.buttonText}>Cancel</Text>
171
+ </TouchableOpacity>
172
+ </View>
173
+ </View>
174
+ );
175
+ }
176
+
59
177
  return (
60
178
  <View style={styles.container}>
61
- {/* Camera View - In real implementation, use react-native-vision-camera */}
179
+ {/* Camera View */}
62
180
  <View style={styles.cameraContainer}>
63
- <View style={styles.mockCamera}>
64
- <Text style={styles.mockCameraText}>Camera View</Text>
65
- <Text style={styles.mockCameraSubtext}>
66
- (In production, this uses react-native-vision-camera)
67
- </Text>
68
- </View>
181
+ <Camera
182
+ ref={cameraRef}
183
+ style={StyleSheet.absoluteFill}
184
+ device={device}
185
+ isActive={true}
186
+ photo={true}
187
+ />
69
188
 
70
189
  {/* Document Frame Overlay */}
71
190
  <View style={styles.overlay}>
@@ -255,6 +374,19 @@ const styles = StyleSheet.create({
255
374
  buttonPlaceholder: {
256
375
  width: 80,
257
376
  },
377
+ permissionContainer: {
378
+ flex: 1,
379
+ justifyContent: 'center',
380
+ alignItems: 'center',
381
+ padding: 32,
382
+ backgroundColor: '#000000',
383
+ },
384
+ permissionText: {
385
+ color: '#FFFFFF',
386
+ fontSize: 18,
387
+ textAlign: 'center',
388
+ marginBottom: 24,
389
+ },
258
390
  });
259
391
 
260
392
  export default CameraCapture;
@@ -3,7 +3,7 @@
3
3
  * Shows initial instructions before starting verification
4
4
  */
5
5
 
6
- import React, { useEffect } from 'react';
6
+ import React, { useEffect, useState } from 'react';
7
7
  import {
8
8
  View,
9
9
  Text,
@@ -30,14 +30,22 @@ export const InstructionsScreen: React.FC<InstructionsScreenProps> = ({
30
30
  onStart,
31
31
  styles: customStyles,
32
32
  }) => {
33
- // Set language when prop changes
33
+ const [strings, setStrings] = useState(() => {
34
+ // Set initial language
35
+ if (language) {
36
+ setLanguage(language);
37
+ }
38
+ return getStrings();
39
+ });
40
+
41
+ // Update strings when language changes
34
42
  useEffect(() => {
35
43
  if (language) {
36
44
  setLanguage(language);
45
+ // Force re-render by updating strings state
46
+ setStrings(getStrings());
37
47
  }
38
48
  }, [language]);
39
-
40
- const strings = getStrings();
41
49
 
42
50
  // Get tips and privacy message based on language
43
51
  const tipsContent = getTipsContent(language);
@@ -11,7 +11,11 @@ import {
11
11
  TouchableOpacity,
12
12
  Animated,
13
13
  Easing,
14
+ Platform,
15
+ Alert,
14
16
  } from 'react-native';
17
+ import { Camera, useCameraDevice, useCameraPermission, useFrameProcessor } from 'react-native-vision-camera';
18
+ import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
15
19
  import { ThemeConfig, LivenessInstruction } from '@hexar/biometric-identity-sdk-core';
16
20
 
17
21
  // Challenge action configuration (matches backend response)
@@ -118,6 +122,12 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
118
122
  const [overallProgress, setOverallProgress] = useState(0);
119
123
  const [completedChallenges, setCompletedChallenges] = useState<string[]>([]);
120
124
  const [frames, setFrames] = useState<string[]>([]);
125
+ const [hasPermission, setHasPermission] = useState(false);
126
+
127
+ // Camera
128
+ const cameraRef = useRef<Camera>(null);
129
+ const { hasPermission: cameraPermission, requestPermission } = useCameraPermission();
130
+ const device = useCameraDevice('front');
121
131
 
122
132
  // Animations
123
133
  const fadeAnim = useRef(new Animated.Value(0)).current;
@@ -129,10 +139,48 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
129
139
  // Refs
130
140
  const recordingStartTime = useRef<number>(0);
131
141
  const frameInterval = useRef<NodeJS.Timeout | null>(null);
142
+ const frameCaptureInterval = useRef<NodeJS.Timeout | null>(null);
132
143
 
133
144
  // Calculate total duration from challenges
134
145
  const totalDuration = duration || challenges.reduce((sum, c) => sum + c.duration_ms, 0) + 2000;
135
146
 
147
+ // Check camera permissions
148
+ useEffect(() => {
149
+ const checkPermissions = async () => {
150
+ try {
151
+ // First check if we already have permission from the hook
152
+ if (cameraPermission) {
153
+ setHasPermission(true);
154
+ return;
155
+ }
156
+
157
+ // Otherwise, request permission using the hook
158
+ const granted = await requestPermission();
159
+ setHasPermission(granted);
160
+
161
+ // If hook method didn't work, try platform-specific request
162
+ if (!granted) {
163
+ if (Platform.OS === 'ios') {
164
+ const result = await request(PERMISSIONS.IOS.CAMERA);
165
+ setHasPermission(result === RESULTS.GRANTED);
166
+ } else {
167
+ const result = await request(PERMISSIONS.ANDROID.CAMERA);
168
+ setHasPermission(result === RESULTS.GRANTED);
169
+ }
170
+ }
171
+ } catch (error) {
172
+ console.error('Permission check error:', error);
173
+ setHasPermission(false);
174
+ Alert.alert(
175
+ 'Camera Permission Required',
176
+ 'Please enable camera access in your device settings to record your face video.'
177
+ );
178
+ }
179
+ };
180
+
181
+ checkPermissions();
182
+ }, [cameraPermission, requestPermission]);
183
+
136
184
  // Initialize challenges
137
185
  useEffect(() => {
138
186
  const initChallenges = async () => {
@@ -251,18 +299,47 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
251
299
  }, [arrowAnim]);
252
300
 
253
301
  // Start recording
254
- const startRecording = useCallback(() => {
302
+ const startRecording = useCallback(async () => {
255
303
  setPhase('recording');
256
304
  recordingStartTime.current = Date.now();
257
305
 
258
- // Simulate frame capture (in real implementation, this would capture actual camera frames)
259
- frameInterval.current = setInterval(() => {
260
- setFrames(prev => [...prev, `frame_${Date.now()}`]);
261
- }, 100); // Capture ~10 FPS
306
+ // Start capturing frames from camera
307
+ if (cameraRef.current && device) {
308
+ // Capture frames periodically (every 100ms = ~10 FPS)
309
+ frameCaptureInterval.current = setInterval(async () => {
310
+ try {
311
+ // Take a photo frame (react-native-vision-camera doesn't have direct frame access in v4)
312
+ // We'll use takeSnapshot or capture frames via photo
313
+ const photo = await cameraRef.current?.takePhoto({
314
+ flash: 'off',
315
+ });
316
+
317
+ if (photo) {
318
+ // Convert to base64 if possible, otherwise use file path
319
+ try {
320
+ const RNFS = require('react-native-fs');
321
+ const base64 = await RNFS.readFile(photo.path, 'base64');
322
+ setFrames(prev => [...prev, base64]);
323
+ } catch (fsError) {
324
+ // Fallback: use file path
325
+ setFrames(prev => [...prev, photo.path]);
326
+ }
327
+ }
328
+ } catch (error) {
329
+ console.warn('Frame capture error:', error);
330
+ // Continue even if one frame fails
331
+ }
332
+ }, 100);
333
+ } else {
334
+ // Fallback: simulate frames if camera not available
335
+ frameInterval.current = setInterval(() => {
336
+ setFrames(prev => [...prev, `frame_${Date.now()}`]);
337
+ }, 100);
338
+ }
262
339
 
263
340
  // Start first challenge
264
341
  runChallenge(0);
265
- }, []);
342
+ }, [device]);
266
343
 
267
344
  // Run a specific challenge
268
345
  const runChallenge = useCallback((index: number) => {
@@ -332,17 +409,20 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
332
409
  if (frameInterval.current) {
333
410
  clearInterval(frameInterval.current);
334
411
  }
412
+ if (frameCaptureInterval.current) {
413
+ clearInterval(frameCaptureInterval.current);
414
+ }
335
415
 
336
416
  setPhase('processing');
337
417
  setOverallProgress(100);
338
418
 
339
- // Simulate processing delay
419
+ // Process and return result
340
420
  setTimeout(() => {
341
421
  const result: VideoRecordingResult = {
342
422
  frames,
343
423
  duration: Date.now() - recordingStartTime.current,
344
424
  instructionsFollowed: completedChallenges.length === challenges.length,
345
- qualityScore: 85 + Math.random() * 10, // Simulated quality score
425
+ qualityScore: frames.length > 0 ? 85 + Math.random() * 10 : 0, // Quality based on frames captured
346
426
  challengesCompleted: completedChallenges,
347
427
  sessionId,
348
428
  };
@@ -407,13 +487,54 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
407
487
  return null;
408
488
  };
409
489
 
490
+ if (!hasPermission) {
491
+ return (
492
+ <View style={styles.container}>
493
+ <View style={styles.permissionContainer}>
494
+ <Text style={styles.permissionText}>Camera permission is required</Text>
495
+ <TouchableOpacity
496
+ style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
497
+ onPress={onCancel}
498
+ >
499
+ <Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
500
+ Cancel
501
+ </Text>
502
+ </TouchableOpacity>
503
+ </View>
504
+ </View>
505
+ );
506
+ }
507
+
508
+ if (!device) {
509
+ return (
510
+ <View style={styles.container}>
511
+ <View style={styles.permissionContainer}>
512
+ <Text style={styles.permissionText}>Camera not available</Text>
513
+ <TouchableOpacity
514
+ style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
515
+ onPress={onCancel}
516
+ >
517
+ <Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
518
+ Cancel
519
+ </Text>
520
+ </TouchableOpacity>
521
+ </View>
522
+ </View>
523
+ );
524
+ }
525
+
410
526
  return (
411
527
  <View style={styles.container}>
412
528
  {/* Camera View */}
413
529
  <View style={styles.cameraContainer}>
414
- <View style={styles.mockCamera}>
415
- <Text style={styles.mockCameraText}>Front Camera</Text>
416
- </View>
530
+ <Camera
531
+ ref={cameraRef}
532
+ style={StyleSheet.absoluteFill}
533
+ device={device}
534
+ isActive={phase === 'recording' || phase === 'countdown'}
535
+ video={false}
536
+ photo={true}
537
+ />
417
538
 
418
539
  {/* Face Oval Overlay */}
419
540
  <View style={styles.overlay}>
@@ -766,6 +887,19 @@ const styles = StyleSheet.create({
766
887
  fontSize: 20,
767
888
  fontWeight: '600',
768
889
  },
890
+ permissionContainer: {
891
+ flex: 1,
892
+ justifyContent: 'center',
893
+ alignItems: 'center',
894
+ padding: 32,
895
+ backgroundColor: '#000000',
896
+ },
897
+ permissionText: {
898
+ color: '#FFFFFF',
899
+ fontSize: 18,
900
+ textAlign: 'center',
901
+ marginBottom: 24,
902
+ },
769
903
  bottomContainer: {
770
904
  paddingVertical: 32,
771
905
  paddingHorizontal: 24,