omnipay-reactnative-sdk 1.2.2-beta.3 → 1.2.2-beta.6

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.
Files changed (135) hide show
  1. package/README.md +93 -43
  2. package/android/build.gradle +16 -15
  3. package/android/src/main/AndroidManifest.xml +1 -1
  4. package/android/src/main/java/com/omniretail/omnipay/OmnipayActivityPackage.java +2 -2
  5. package/android/src/main/java/com/omniretail/omnipay/OmnipayLivenessCameraView.java +153 -0
  6. package/android/src/main/java/com/omniretail/omnipay/OmnipayLivenessCameraViewManager.java +49 -0
  7. package/android/src/main/java/com/omniretail/omnipay/OmnipayLivenessModule.java +524 -0
  8. package/ios/OmnipayLivenessCameraView.h +15 -0
  9. package/ios/OmnipayLivenessCameraView.m +80 -0
  10. package/ios/OmnipayLivenessCameraViewManager.m +19 -0
  11. package/ios/OmnipayLivenessModule.h +38 -0
  12. package/ios/OmnipayLivenessModule.m +554 -0
  13. package/lib/commonjs/components/OmnipayProvider.js +2 -66
  14. package/lib/commonjs/components/OmnipayProvider.js.map +1 -1
  15. package/lib/commonjs/components/OmnipayView.js.map +1 -1
  16. package/lib/commonjs/components/biometrics/FaceVerification.js +252 -345
  17. package/lib/commonjs/components/biometrics/FaceVerification.js.map +1 -1
  18. package/lib/commonjs/components/biometrics/LivenessDetection.js +90 -198
  19. package/lib/commonjs/components/biometrics/LivenessDetection.js.map +1 -1
  20. package/lib/commonjs/components/biometrics/OmnipayLivenessCameraView.js +15 -0
  21. package/lib/commonjs/components/biometrics/OmnipayLivenessCameraView.js.map +1 -0
  22. package/lib/commonjs/components/biometrics/PermissionManager.js +279 -0
  23. package/lib/commonjs/components/biometrics/PermissionManager.js.map +1 -0
  24. package/lib/commonjs/components/biometrics/index.js +45 -0
  25. package/lib/commonjs/components/biometrics/index.js.map +1 -0
  26. package/lib/commonjs/components/biometrics/types.js +17 -0
  27. package/lib/commonjs/components/biometrics/types.js.map +1 -0
  28. package/lib/commonjs/components/views/BvnVerification.js.map +1 -1
  29. package/lib/commonjs/components/views/PaylaterAgreement.js.map +1 -1
  30. package/lib/commonjs/components/views/Registration.js.map +1 -1
  31. package/lib/commonjs/index.js +23 -18
  32. package/lib/commonjs/index.js.map +1 -1
  33. package/lib/module/components/OmnipayProvider.js +3 -67
  34. package/lib/module/components/OmnipayProvider.js.map +1 -1
  35. package/lib/module/components/OmnipayView.js.map +1 -1
  36. package/lib/module/components/biometrics/FaceVerification.js +254 -346
  37. package/lib/module/components/biometrics/FaceVerification.js.map +1 -1
  38. package/lib/module/components/biometrics/LivenessDetection.js +75 -197
  39. package/lib/module/components/biometrics/LivenessDetection.js.map +1 -1
  40. package/lib/module/components/biometrics/OmnipayLivenessCameraView.js +7 -0
  41. package/lib/module/components/biometrics/OmnipayLivenessCameraView.js.map +1 -0
  42. package/lib/module/components/biometrics/PermissionManager.js +272 -0
  43. package/lib/module/components/biometrics/PermissionManager.js.map +1 -0
  44. package/lib/module/components/biometrics/index.js +12 -0
  45. package/lib/module/components/biometrics/index.js.map +1 -0
  46. package/lib/module/components/biometrics/types.js +16 -0
  47. package/lib/module/components/biometrics/types.js.map +1 -0
  48. package/lib/module/components/views/BvnVerification.js.map +1 -1
  49. package/lib/module/components/views/PaylaterAgreement.js.map +1 -1
  50. package/lib/module/components/views/Registration.js.map +1 -1
  51. package/lib/module/index.js +5 -4
  52. package/lib/module/index.js.map +1 -1
  53. package/lib/typescript/{src/components → components}/OmnipayProvider.d.ts +1 -1
  54. package/lib/typescript/components/OmnipayProvider.d.ts.map +1 -0
  55. package/lib/typescript/{src/components → components}/OmnipayView.d.ts +21 -20
  56. package/lib/typescript/components/OmnipayView.d.ts.map +1 -0
  57. package/lib/typescript/components/biometrics/FaceVerification.d.ts +11 -0
  58. package/lib/typescript/components/biometrics/FaceVerification.d.ts.map +1 -0
  59. package/lib/typescript/components/biometrics/LivenessDetection.d.ts +33 -0
  60. package/lib/typescript/components/biometrics/LivenessDetection.d.ts.map +1 -0
  61. package/lib/typescript/components/biometrics/OmnipayLivenessCameraView.d.ts +18 -0
  62. package/lib/typescript/components/biometrics/OmnipayLivenessCameraView.d.ts.map +1 -0
  63. package/lib/typescript/components/biometrics/PermissionManager.d.ts +58 -0
  64. package/lib/typescript/components/biometrics/PermissionManager.d.ts.map +1 -0
  65. package/lib/typescript/components/biometrics/index.d.ts +5 -0
  66. package/lib/typescript/components/biometrics/index.d.ts.map +1 -0
  67. package/lib/typescript/components/biometrics/types.d.ts +73 -0
  68. package/lib/typescript/components/biometrics/types.d.ts.map +1 -0
  69. package/lib/typescript/{src/components → components}/views/BvnVerification.d.ts +2 -1
  70. package/lib/typescript/components/views/BvnVerification.d.ts.map +1 -0
  71. package/lib/typescript/{src/components → components}/views/PaylaterAgreement.d.ts +2 -1
  72. package/lib/typescript/components/views/PaylaterAgreement.d.ts.map +1 -0
  73. package/lib/typescript/{src/components → components}/views/Registration.d.ts +2 -1
  74. package/lib/typescript/components/views/Registration.d.ts.map +1 -0
  75. package/lib/typescript/functions.d.ts.map +1 -0
  76. package/lib/typescript/hooks/useOmnipay.d.ts +28 -0
  77. package/lib/typescript/hooks/useOmnipay.d.ts.map +1 -0
  78. package/lib/typescript/index.d.ts +7 -0
  79. package/lib/typescript/index.d.ts.map +1 -0
  80. package/lib/typescript/lib/colors.d.ts.map +1 -0
  81. package/lib/typescript/lib/config.d.ts.map +1 -0
  82. package/omnipay-reactnative-sdk.podspec +32 -29
  83. package/package.json +16 -11
  84. package/src/components/OmnipayProvider.tsx +3 -106
  85. package/src/components/OmnipayView.tsx +1 -1
  86. package/src/components/biometrics/FaceVerification.tsx +291 -368
  87. package/src/components/biometrics/LivenessDetection.ts +113 -250
  88. package/src/components/biometrics/OmnipayLivenessCameraView.tsx +19 -0
  89. package/src/components/biometrics/PermissionManager.ts +317 -0
  90. package/src/components/biometrics/index.ts +11 -0
  91. package/src/components/biometrics/types.ts +86 -0
  92. package/src/components/views/BvnVerification.tsx +1 -1
  93. package/src/components/views/PaylaterAgreement.tsx +1 -1
  94. package/src/components/views/Registration.tsx +1 -1
  95. package/src/index.tsx +4 -15
  96. package/android/src/main/java/com/omniretail/omnipay/LivenessCameraViewManager.java +0 -116
  97. package/android/src/main/java/com/omniretail/omnipay/LivenessDetectionModule.java +0 -588
  98. package/ios/LivenessCameraView.h +0 -22
  99. package/ios/LivenessCameraView.m +0 -135
  100. package/ios/LivenessCameraViewManager.h +0 -12
  101. package/ios/LivenessCameraViewManager.m +0 -24
  102. package/ios/LivenessDetectionModule.h +0 -46
  103. package/ios/LivenessDetectionModule.m +0 -603
  104. package/lib/commonjs/components/biometrics/LivenessCameraView.js +0 -45
  105. package/lib/commonjs/components/biometrics/LivenessCameraView.js.map +0 -1
  106. package/lib/module/components/biometrics/LivenessCameraView.js +0 -39
  107. package/lib/module/components/biometrics/LivenessCameraView.js.map +0 -1
  108. package/lib/typescript/demo/src/App.d.ts +0 -3
  109. package/lib/typescript/demo/src/App.d.ts.map +0 -1
  110. package/lib/typescript/demo/src/Body.d.ts +0 -3
  111. package/lib/typescript/demo/src/Body.d.ts.map +0 -1
  112. package/lib/typescript/demo/src/NotificationsExample.d.ts +0 -4
  113. package/lib/typescript/demo/src/NotificationsExample.d.ts.map +0 -1
  114. package/lib/typescript/src/components/OmnipayProvider.d.ts.map +0 -1
  115. package/lib/typescript/src/components/OmnipayView.d.ts.map +0 -1
  116. package/lib/typescript/src/components/biometrics/FaceVerification.d.ts +0 -12
  117. package/lib/typescript/src/components/biometrics/FaceVerification.d.ts.map +0 -1
  118. package/lib/typescript/src/components/biometrics/LivenessCameraView.d.ts +0 -22
  119. package/lib/typescript/src/components/biometrics/LivenessCameraView.d.ts.map +0 -1
  120. package/lib/typescript/src/components/biometrics/LivenessDetection.d.ts +0 -73
  121. package/lib/typescript/src/components/biometrics/LivenessDetection.d.ts.map +0 -1
  122. package/lib/typescript/src/components/views/BvnVerification.d.ts.map +0 -1
  123. package/lib/typescript/src/components/views/PaylaterAgreement.d.ts.map +0 -1
  124. package/lib/typescript/src/components/views/Registration.d.ts.map +0 -1
  125. package/lib/typescript/src/functions.d.ts.map +0 -1
  126. package/lib/typescript/src/hooks/useOmnipay.d.ts +0 -28
  127. package/lib/typescript/src/hooks/useOmnipay.d.ts.map +0 -1
  128. package/lib/typescript/src/index.d.ts +0 -8
  129. package/lib/typescript/src/index.d.ts.map +0 -1
  130. package/lib/typescript/src/lib/colors.d.ts.map +0 -1
  131. package/lib/typescript/src/lib/config.d.ts.map +0 -1
  132. package/src/components/biometrics/LivenessCameraView.tsx +0 -61
  133. /package/lib/typescript/{src/functions.d.ts → functions.d.ts} +0 -0
  134. /package/lib/typescript/{src/lib → lib}/colors.d.ts +0 -0
  135. /package/lib/typescript/{src/lib → lib}/config.d.ts +0 -0
@@ -0,0 +1,524 @@
1
+ package com.omniretail.omnipay;
2
+
3
+ import android.app.Activity;
4
+ import android.content.Context;
5
+ import android.graphics.Bitmap;
6
+ import android.graphics.Matrix;
7
+ import android.graphics.Rect;
8
+ import android.util.Base64;
9
+ import android.util.Log;
10
+ import android.util.Size;
11
+
12
+ import androidx.annotation.NonNull;
13
+ import androidx.camera.core.CameraSelector;
14
+ import androidx.camera.core.ImageAnalysis;
15
+ import androidx.camera.core.ImageCapture;
16
+ import androidx.camera.core.ImageCaptureException;
17
+ import androidx.camera.core.ImageProxy;
18
+ import androidx.camera.core.Preview;
19
+ import androidx.camera.lifecycle.ProcessCameraProvider;
20
+ import androidx.camera.view.PreviewView;
21
+ import androidx.core.content.ContextCompat;
22
+ import androidx.lifecycle.LifecycleOwner;
23
+
24
+ import com.facebook.react.bridge.Arguments;
25
+ import com.facebook.react.bridge.Promise;
26
+ import com.facebook.react.bridge.ReactApplicationContext;
27
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
28
+ import com.facebook.react.bridge.ReactMethod;
29
+ import com.facebook.react.bridge.ReadableMap;
30
+ import com.facebook.react.bridge.WritableArray;
31
+ import com.facebook.react.bridge.WritableMap;
32
+ import com.facebook.react.modules.core.DeviceEventManagerModule;
33
+ import com.google.android.gms.tasks.OnFailureListener;
34
+ import com.google.android.gms.tasks.OnSuccessListener;
35
+ import com.google.common.util.concurrent.ListenableFuture;
36
+ import com.google.mlkit.vision.common.InputImage;
37
+ import com.google.mlkit.vision.face.Face;
38
+ import com.google.mlkit.vision.face.FaceDetection;
39
+ import com.google.mlkit.vision.face.FaceDetector;
40
+ import com.google.mlkit.vision.face.FaceDetectorOptions;
41
+
42
+ import java.io.ByteArrayOutputStream;
43
+ import java.util.List;
44
+ import java.util.concurrent.ExecutionException;
45
+ import java.util.concurrent.ExecutorService;
46
+ import java.util.concurrent.Executors;
47
+
48
+ public class OmnipayLivenessModule extends ReactContextBaseJavaModule {
49
+ private static final String TAG = "OmnipayLiveness";
50
+
51
+ // Challenge constants
52
+ private static final String CHALLENGE_SMILE = "smile";
53
+ private static final String CHALLENGE_BLINK = "blink";
54
+ private static final String CHALLENGE_TURN_LEFT = "turnLeft";
55
+ private static final String CHALLENGE_TURN_RIGHT = "turnRight";
56
+
57
+ // Detection thresholds
58
+ private static final float SMILE_THRESHOLD = 0.8f;
59
+ private static final float HEAD_TURN_THRESHOLD = 15.0f;
60
+ private static final int BLINK_FRAMES_THRESHOLD = 3;
61
+
62
+ private ReactApplicationContext reactContext;
63
+ private FaceDetector faceDetector;
64
+ private ExecutorService cameraExecutor;
65
+ private ProcessCameraProvider cameraProvider;
66
+ private ImageCapture imageCapture;
67
+ private PreviewView previewView;
68
+
69
+ // Detection state
70
+ private boolean isDetectionRunning = false;
71
+ private String currentChallenge = null;
72
+ private WritableArray challenges;
73
+ private int currentChallengeIndex = 0;
74
+ private long challengeStartTime = 0;
75
+ private int challengeTimeoutMs = 10000; // 10 seconds default
76
+
77
+ // Blink detection state
78
+ private boolean previousEyeOpenState = true;
79
+ private int blinkCounter = 0;
80
+ private int eyesClosedFrames = 0;
81
+
82
+ // Challenge completion tracking
83
+ private boolean challengeCompleted = false;
84
+
85
+ public OmnipayLivenessModule(ReactApplicationContext reactContext) {
86
+ super(reactContext);
87
+ this.reactContext = reactContext;
88
+ initializeFaceDetector();
89
+ this.cameraExecutor = Executors.newSingleThreadExecutor();
90
+ }
91
+
92
+ @Override
93
+ public String getName() {
94
+ return "OmnipayLivenessModule";
95
+ }
96
+
97
+ private void initializeFaceDetector() {
98
+ FaceDetectorOptions options = new FaceDetectorOptions.Builder()
99
+ .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
100
+ .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
101
+ .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
102
+ .setMinFaceSize(0.3f)
103
+ .enableTracking()
104
+ .build();
105
+
106
+ faceDetector = FaceDetection.getClient(options);
107
+ }
108
+
109
+ @ReactMethod
110
+ public void isSupported(Promise promise) {
111
+ try {
112
+ // Check if ML Kit face detection is available
113
+ promise.resolve(true);
114
+ } catch (Exception e) {
115
+ Log.e(TAG, "Error checking support", e);
116
+ promise.resolve(false);
117
+ }
118
+ }
119
+
120
+ @ReactMethod
121
+ public void startLivenessDetection(ReadableMap config, Promise promise) {
122
+ try {
123
+ if (isDetectionRunning) {
124
+ promise.reject("DETECTION_RUNNING", "Detection is already running");
125
+ return;
126
+ }
127
+
128
+ // Parse configuration
129
+ parseConfig(config);
130
+
131
+ Activity activity = getCurrentActivity();
132
+ if (activity == null) {
133
+ promise.reject("NO_ACTIVITY", "No current activity available");
134
+ return;
135
+ }
136
+
137
+ // Initialize camera
138
+ initializeCamera(activity, promise);
139
+
140
+ } catch (Exception e) {
141
+ Log.e(TAG, "Error starting liveness detection", e);
142
+ promise.reject("START_ERROR", e.getMessage());
143
+ }
144
+ }
145
+
146
+ @ReactMethod
147
+ public void stopDetection(Promise promise) {
148
+ try {
149
+ stopLivenessDetection();
150
+ promise.resolve(null);
151
+ } catch (Exception e) {
152
+ Log.e(TAG, "Error stopping detection", e);
153
+ promise.reject("STOP_ERROR", e.getMessage());
154
+ }
155
+ }
156
+
157
+ private void parseConfig(ReadableMap config) {
158
+ // Parse challenges
159
+ if (config.hasKey("challenges")) {
160
+ challenges = config.getArray("challenges");
161
+ } else {
162
+ // Default challenges
163
+ challenges = Arguments.createArray();
164
+ challenges.pushString(CHALLENGE_SMILE);
165
+ challenges.pushString(CHALLENGE_BLINK);
166
+ challenges.pushString(CHALLENGE_TURN_LEFT);
167
+ challenges.pushString(CHALLENGE_TURN_RIGHT);
168
+ }
169
+
170
+ // Parse timeout
171
+ if (config.hasKey("challengeTimeout")) {
172
+ challengeTimeoutMs = config.getInt("challengeTimeout") * 1000;
173
+ }
174
+
175
+ currentChallengeIndex = 0;
176
+ }
177
+
178
+ private void initializeCamera(Activity activity, Promise promise) {
179
+ ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
180
+ ProcessCameraProvider.getInstance(activity);
181
+
182
+ cameraProviderFuture.addListener(() -> {
183
+ try {
184
+ cameraProvider = cameraProviderFuture.get();
185
+ startCamera(activity, promise);
186
+ } catch (ExecutionException | InterruptedException e) {
187
+ Log.e(TAG, "Error getting camera provider", e);
188
+ promise.reject("CAMERA_ERROR", "Failed to initialize camera");
189
+ }
190
+ }, ContextCompat.getMainExecutor(activity));
191
+ }
192
+
193
+ private void startCamera(Activity activity, Promise promise) {
194
+ // Preview use case
195
+ Preview preview = new Preview.Builder().build();
196
+
197
+ // Image capture use case
198
+ imageCapture = new ImageCapture.Builder().build();
199
+
200
+ // Image analysis use case for face detection
201
+ ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
202
+ .setTargetResolution(new Size(640, 480))
203
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
204
+ .build();
205
+
206
+ imageAnalysis.setAnalyzer(cameraExecutor, this::analyzeImage);
207
+
208
+ // Select front camera
209
+ CameraSelector cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA;
210
+
211
+ try {
212
+ // Unbind use cases before rebinding
213
+ cameraProvider.unbindAll();
214
+
215
+ // Bind use cases to camera
216
+ cameraProvider.bindToLifecycle(
217
+ (LifecycleOwner) activity,
218
+ cameraSelector,
219
+ preview,
220
+ imageCapture,
221
+ imageAnalysis
222
+ );
223
+
224
+ isDetectionRunning = true;
225
+ startNextChallenge();
226
+ promise.resolve(null);
227
+
228
+ } catch (Exception e) {
229
+ Log.e(TAG, "Error starting camera", e);
230
+ promise.reject("CAMERA_START_ERROR", "Failed to start camera");
231
+ }
232
+ }
233
+
234
+ private void analyzeImage(ImageProxy imageProxy) {
235
+ if (!isDetectionRunning || currentChallenge == null) {
236
+ imageProxy.close();
237
+ return;
238
+ }
239
+
240
+ try {
241
+ InputImage image = InputImage.fromMediaImage(
242
+ imageProxy.getImage(),
243
+ imageProxy.getImageInfo().getRotationDegrees()
244
+ );
245
+
246
+ faceDetector.process(image)
247
+ .addOnSuccessListener(faces -> {
248
+ processFaces(faces);
249
+ imageProxy.close();
250
+ })
251
+ .addOnFailureListener(e -> {
252
+ Log.e(TAG, "Face detection failed", e);
253
+ imageProxy.close();
254
+ });
255
+
256
+ } catch (Exception e) {
257
+ Log.e(TAG, "Error analyzing image", e);
258
+ imageProxy.close();
259
+ }
260
+ }
261
+
262
+ private void processFaces(List<Face> faces) {
263
+ if (faces.isEmpty()) {
264
+ // No face detected
265
+ return;
266
+ }
267
+
268
+ Face face = faces.get(0); // Use the first detected face
269
+
270
+ // Check if challenge timeout exceeded
271
+ if (System.currentTimeMillis() - challengeStartTime > challengeTimeoutMs) {
272
+ onChallengeFailure("Challenge timeout");
273
+ return;
274
+ }
275
+
276
+ if (challengeCompleted) {
277
+ return;
278
+ }
279
+
280
+ switch (currentChallenge) {
281
+ case CHALLENGE_SMILE:
282
+ checkSmile(face);
283
+ break;
284
+ case CHALLENGE_BLINK:
285
+ checkBlink(face);
286
+ break;
287
+ case CHALLENGE_TURN_LEFT:
288
+ checkHeadTurn(face, -HEAD_TURN_THRESHOLD);
289
+ break;
290
+ case CHALLENGE_TURN_RIGHT:
291
+ checkHeadTurn(face, HEAD_TURN_THRESHOLD);
292
+ break;
293
+ }
294
+ }
295
+
296
+ private void checkSmile(Face face) {
297
+ float smilingProbability = face.getSmilingProbability() != null ?
298
+ face.getSmilingProbability() : 0f;
299
+
300
+ if (smilingProbability > SMILE_THRESHOLD) {
301
+ onChallengeSuccess();
302
+ }
303
+ }
304
+
305
+ private void checkBlink(Face face) {
306
+ float leftEyeOpenProbability = face.getLeftEyeOpenProbability() != null ?
307
+ face.getLeftEyeOpenProbability() : 1f;
308
+ float rightEyeOpenProbability = face.getRightEyeOpenProbability() != null ?
309
+ face.getRightEyeOpenProbability() : 1f;
310
+
311
+ boolean eyesOpen = leftEyeOpenProbability > 0.5f && rightEyeOpenProbability > 0.5f;
312
+
313
+ if (!eyesOpen) {
314
+ eyesClosedFrames++;
315
+ } else {
316
+ if (eyesClosedFrames >= BLINK_FRAMES_THRESHOLD && !previousEyeOpenState) {
317
+ blinkCounter++;
318
+ if (blinkCounter >= 1) { // Single blink required
319
+ onChallengeSuccess();
320
+ }
321
+ }
322
+ eyesClosedFrames = 0;
323
+ }
324
+
325
+ previousEyeOpenState = eyesOpen;
326
+ }
327
+
328
+ private void checkHeadTurn(Face face, float targetYaw) {
329
+ float headEulerAngleY = face.getHeadEulerAngleY();
330
+
331
+ if (targetYaw < 0) { // Turn left
332
+ if (headEulerAngleY < targetYaw) {
333
+ onChallengeSuccess();
334
+ }
335
+ } else { // Turn right
336
+ if (headEulerAngleY > targetYaw) {
337
+ onChallengeSuccess();
338
+ }
339
+ }
340
+ }
341
+
342
+ private void onChallengeSuccess() {
343
+ if (challengeCompleted) return;
344
+
345
+ challengeCompleted = true;
346
+ long duration = System.currentTimeMillis() - challengeStartTime;
347
+
348
+ // Emit challenge success event
349
+ WritableMap params = Arguments.createMap();
350
+ params.putString("challenge", currentChallenge);
351
+ WritableMap result = Arguments.createMap();
352
+ result.putString("challenge", currentChallenge);
353
+ result.putBoolean("success", true);
354
+ result.putDouble("duration", duration);
355
+ result.putDouble("confidence", 0.9); // Mock confidence for now
356
+ params.putMap("result", result);
357
+
358
+ sendEvent("onChallengeSuccess", params);
359
+
360
+ // Move to next challenge or complete
361
+ currentChallengeIndex++;
362
+ if (currentChallengeIndex < challenges.size()) {
363
+ // Delay before next challenge
364
+ cameraExecutor.execute(() -> {
365
+ try {
366
+ Thread.sleep(1000); // 1 second delay
367
+ startNextChallenge();
368
+ } catch (InterruptedException e) {
369
+ Thread.currentThread().interrupt();
370
+ }
371
+ });
372
+ } else {
373
+ // All challenges completed
374
+ onAllChallengesComplete();
375
+ }
376
+ }
377
+
378
+ private void onChallengeFailure(String reason) {
379
+ if (challengeCompleted) return;
380
+
381
+ challengeCompleted = true;
382
+
383
+ WritableMap params = Arguments.createMap();
384
+ params.putString("challenge", currentChallenge);
385
+ params.putString("reason", reason);
386
+
387
+ sendEvent("onChallengeFailure", params);
388
+
389
+ // Stop detection
390
+ stopLivenessDetection();
391
+
392
+ WritableMap failureParams = Arguments.createMap();
393
+ failureParams.putString("reason", reason);
394
+ sendEvent("onDetectionFailed", failureParams);
395
+ }
396
+
397
+ private void startNextChallenge() {
398
+ if (currentChallengeIndex >= challenges.size()) {
399
+ return;
400
+ }
401
+
402
+ currentChallenge = challenges.getString(currentChallengeIndex);
403
+ challengeStartTime = System.currentTimeMillis();
404
+ challengeCompleted = false;
405
+
406
+ // Reset blink detection state
407
+ if (CHALLENGE_BLINK.equals(currentChallenge)) {
408
+ blinkCounter = 0;
409
+ eyesClosedFrames = 0;
410
+ previousEyeOpenState = true;
411
+ }
412
+
413
+ // Emit challenge start event
414
+ WritableMap params = Arguments.createMap();
415
+ params.putString("challenge", currentChallenge);
416
+ sendEvent("onChallengeStart", params);
417
+ }
418
+
419
+ private void onAllChallengesComplete() {
420
+ sendEvent("onAllChallengesComplete", null);
421
+
422
+ // Capture final screenshot
423
+ captureScreenshot();
424
+ }
425
+
426
+ private void captureScreenshot() {
427
+ if (imageCapture == null) {
428
+ sendDetectionResult(false, null, "Failed to capture screenshot");
429
+ return;
430
+ }
431
+
432
+ imageCapture.takePicture(
433
+ ContextCompat.getMainExecutor(reactContext),
434
+ new ImageCapture.OnImageCapturedCallback() {
435
+ @Override
436
+ public void onCaptureSuccess(@NonNull ImageProxy image) {
437
+ // Convert to bitmap and encode as base64
438
+ String base64Image = convertImageToBase64(image);
439
+
440
+ WritableMap params = Arguments.createMap();
441
+ params.putString("screenshot", base64Image);
442
+ sendEvent("onScreenshotCaptured", params);
443
+
444
+ sendDetectionResult(true, base64Image, null);
445
+ image.close();
446
+ }
447
+
448
+ @Override
449
+ public void onError(@NonNull ImageCaptureException exception) {
450
+ Log.e(TAG, "Screenshot capture failed", exception);
451
+ sendDetectionResult(false, null, "Screenshot capture failed");
452
+ }
453
+ }
454
+ );
455
+ }
456
+
457
+ private String convertImageToBase64(ImageProxy image) {
458
+ // This is a simplified conversion - in real implementation,
459
+ // you'd need to properly convert ImageProxy to Bitmap
460
+ try {
461
+ // Mock base64 string for now
462
+ return "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ...";
463
+ } catch (Exception e) {
464
+ Log.e(TAG, "Error converting image to base64", e);
465
+ return "";
466
+ }
467
+ }
468
+
469
+ private void sendDetectionResult(boolean success, String screenshot, String failureReason) {
470
+ stopLivenessDetection();
471
+
472
+ WritableMap result = Arguments.createMap();
473
+ result.putBoolean("success", success);
474
+ if (screenshot != null) {
475
+ result.putString("screenshot", screenshot);
476
+ }
477
+ if (failureReason != null) {
478
+ result.putString("failureReason", failureReason);
479
+ }
480
+
481
+ // Create mock challenge results
482
+ WritableArray challengeResults = Arguments.createArray();
483
+ for (int i = 0; i < currentChallengeIndex; i++) {
484
+ WritableMap challengeResult = Arguments.createMap();
485
+ challengeResult.putString("challenge", challenges.getString(i));
486
+ challengeResult.putBoolean("success", true);
487
+ challengeResult.putDouble("duration", 2000); // Mock duration
488
+ challengeResult.putDouble("confidence", 0.9);
489
+ challengeResults.pushMap(challengeResult);
490
+ }
491
+ result.putArray("challengeResults", challengeResults);
492
+ result.putDouble("totalDuration", System.currentTimeMillis() - challengeStartTime);
493
+
494
+ sendEvent("onDetectionComplete", result);
495
+ }
496
+
497
+ private void stopLivenessDetection() {
498
+ isDetectionRunning = false;
499
+ currentChallenge = null;
500
+ currentChallengeIndex = 0;
501
+ challengeCompleted = false;
502
+
503
+ if (cameraProvider != null) {
504
+ cameraProvider.unbindAll();
505
+ }
506
+ }
507
+
508
+ private void sendEvent(String eventName, WritableMap params) {
509
+ reactContext
510
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
511
+ .emit(eventName, params);
512
+ }
513
+
514
+ // Event listener management for React Native
515
+ @ReactMethod
516
+ public void addListener(String eventName) {
517
+ // Required for RN built in Event Emitter Calls
518
+ }
519
+
520
+ @ReactMethod
521
+ public void removeListeners(Integer count) {
522
+ // Required for RN built in Event Emitter Calls
523
+ }
524
+ }
@@ -0,0 +1,15 @@
1
+ #import <UIKit/UIKit.h>
2
+ #import <AVFoundation/AVFoundation.h>
3
+ #import <React/RCTComponent.h>
4
+
5
+ @interface OmnipayLivenessCameraView : UIView
6
+
7
+ @property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;
8
+ @property (nonatomic, copy) RCTBubblingEventBlock onCameraReady;
9
+ @property (nonatomic, copy) RCTBubblingEventBlock onCameraError;
10
+
11
+ - (void)setupCameraPreview:(AVCaptureSession *)captureSession;
12
+ - (void)startPreview;
13
+ - (void)stopPreview;
14
+
15
+ @end
@@ -0,0 +1,80 @@
1
+ #import "OmnipayLivenessCameraView.h"
2
+ #import <React/RCTLog.h>
3
+
4
+ @implementation OmnipayLivenessCameraView
5
+
6
+ - (instancetype)initWithFrame:(CGRect)frame {
7
+ self = [super initWithFrame:frame];
8
+ if (self) {
9
+ [self setupView];
10
+ }
11
+ return self;
12
+ }
13
+
14
+ - (void)setupView {
15
+ self.backgroundColor = [UIColor blackColor];
16
+ self.clipsToBounds = YES;
17
+ }
18
+
19
+ - (void)setupCameraPreview:(AVCaptureSession *)captureSession {
20
+ if (!captureSession) {
21
+ RCTLogError(@"Cannot setup camera preview: capture session is nil");
22
+ if (self.onCameraError) {
23
+ self.onCameraError(@{@"error": @"Capture session is nil"});
24
+ }
25
+ return;
26
+ }
27
+
28
+ // Remove existing preview layer
29
+ if (self.previewLayer) {
30
+ [self.previewLayer removeFromSuperlayer];
31
+ }
32
+
33
+ // Create new preview layer
34
+ self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:captureSession];
35
+ self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
36
+ self.previewLayer.frame = self.bounds;
37
+
38
+ [self.layer addSublayer:self.previewLayer];
39
+
40
+ // Notify that camera is ready
41
+ if (self.onCameraReady) {
42
+ self.onCameraReady(@{@"ready": @YES});
43
+ }
44
+ }
45
+
46
+ - (void)layoutSubviews {
47
+ [super layoutSubviews];
48
+
49
+ // Update preview layer frame when view bounds change
50
+ if (self.previewLayer) {
51
+ self.previewLayer.frame = self.bounds;
52
+ }
53
+ }
54
+
55
+ - (void)startPreview {
56
+ if (self.previewLayer && self.previewLayer.session) {
57
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
58
+ if (![self.previewLayer.session isRunning]) {
59
+ [self.previewLayer.session startRunning];
60
+ }
61
+ });
62
+ }
63
+ }
64
+
65
+ - (void)stopPreview {
66
+ if (self.previewLayer && self.previewLayer.session) {
67
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
68
+ if ([self.previewLayer.session isRunning]) {
69
+ [self.previewLayer.session stopRunning];
70
+ }
71
+ });
72
+ }
73
+ }
74
+
75
+ - (void)removeFromSuperview {
76
+ [self stopPreview];
77
+ [super removeFromSuperview];
78
+ }
79
+
80
+ @end
@@ -0,0 +1,19 @@
1
+ #import <React/RCTViewManager.h>
2
+ #import "OmnipayLivenessCameraView.h"
3
+
4
+ @interface OmnipayLivenessCameraViewManager : RCTViewManager
5
+ @end
6
+
7
+ @implementation OmnipayLivenessCameraViewManager
8
+
9
+ RCT_EXPORT_MODULE(OmnipayLivenessCameraView)
10
+
11
+ - (UIView *)view {
12
+ return [[OmnipayLivenessCameraView alloc] init];
13
+ }
14
+
15
+ // Export event props
16
+ RCT_EXPORT_VIEW_PROPERTY(onCameraReady, RCTBubblingEventBlock)
17
+ RCT_EXPORT_VIEW_PROPERTY(onCameraError, RCTBubblingEventBlock)
18
+
19
+ @end
@@ -0,0 +1,38 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <React/RCTEventEmitter.h>
3
+ #import <Vision/Vision.h>
4
+ #import <AVFoundation/AVFoundation.h>
5
+ #import <UIKit/UIKit.h>
6
+
7
+ @interface OmnipayLivenessModule : RCTEventEmitter <RCTBridgeModule, AVCaptureVideoDataOutputSampleBufferDelegate>
8
+
9
+ @property (nonatomic, strong) AVCaptureSession *captureSession;
10
+ @property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;
11
+ @property (nonatomic, strong) AVCaptureVideoDataOutput *videoDataOutput;
12
+ @property (nonatomic, strong) AVCapturePhotoOutput *photoOutput;
13
+ @property (nonatomic, strong) dispatch_queue_t videoDataOutputQueue;
14
+
15
+ @property (nonatomic, strong) VNDetectFaceRectanglesRequest *faceDetectionRequest;
16
+ @property (nonatomic, strong) VNDetectFaceLandmarksRequest *faceLandmarksRequest;
17
+
18
+ // Detection state
19
+ @property (nonatomic, assign) BOOL isDetectionRunning;
20
+ @property (nonatomic, strong) NSString *currentChallenge;
21
+ @property (nonatomic, strong) NSArray *challenges;
22
+ @property (nonatomic, assign) NSInteger currentChallengeIndex;
23
+ @property (nonatomic, assign) NSTimeInterval challengeStartTime;
24
+ @property (nonatomic, assign) NSTimeInterval challengeTimeoutSeconds;
25
+
26
+ // Blink detection state
27
+ @property (nonatomic, assign) BOOL previousEyeOpenState;
28
+ @property (nonatomic, assign) NSInteger blinkCounter;
29
+ @property (nonatomic, assign) NSInteger eyesClosedFrames;
30
+
31
+ // Challenge completion tracking
32
+ @property (nonatomic, assign) BOOL challengeCompleted;
33
+
34
+ // Detection thresholds
35
+ @property (nonatomic, assign) CGFloat headTurnThreshold;
36
+ @property (nonatomic, assign) NSInteger blinkFramesThreshold;
37
+
38
+ @end