omnipay-reactnative-sdk 1.2.3-beta.9 → 1.2.4

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 (65) hide show
  1. package/README.md +14 -64
  2. package/android/build.gradle +4 -15
  3. package/android/src/main/java/com/omniretail/omnipay/OmnipayActivityPackage.java +0 -5
  4. package/lib/commonjs/components/OmnipayProvider.js +98 -20
  5. package/lib/commonjs/components/OmnipayProvider.js.map +1 -1
  6. package/lib/commonjs/utils/buildUrlWithMetadata.js +14 -0
  7. package/lib/commonjs/utils/buildUrlWithMetadata.js.map +1 -0
  8. package/lib/module/components/OmnipayProvider.js +99 -21
  9. package/lib/module/components/OmnipayProvider.js.map +1 -1
  10. package/lib/module/components/OmnipayView.js.map +1 -1
  11. package/lib/module/components/views/BvnVerification.js.map +1 -1
  12. package/lib/module/components/views/PaylaterAgreement.js.map +1 -1
  13. package/lib/module/components/views/Registration.js.map +1 -1
  14. package/lib/module/utils/buildUrlWithMetadata.js +8 -0
  15. package/lib/module/utils/buildUrlWithMetadata.js.map +1 -0
  16. package/lib/typescript/components/OmnipayProvider.d.ts +5 -2
  17. package/lib/typescript/components/OmnipayProvider.d.ts.map +1 -1
  18. package/lib/typescript/hooks/useOmnipay.d.ts +4 -1
  19. package/lib/typescript/hooks/useOmnipay.d.ts.map +1 -1
  20. package/lib/typescript/utils/buildUrlWithMetadata.d.ts +2 -0
  21. package/lib/typescript/utils/buildUrlWithMetadata.d.ts.map +1 -0
  22. package/package.json +12 -29
  23. package/src/components/OmnipayProvider.tsx +154 -31
  24. package/src/components/OmnipayView.tsx +1 -1
  25. package/src/components/views/BvnVerification.tsx +1 -1
  26. package/src/components/views/PaylaterAgreement.tsx +1 -1
  27. package/src/components/views/Registration.tsx +1 -1
  28. package/src/hooks/useOmnipay.tsx +1 -1
  29. package/src/utils/buildUrlWithMetadata.ts +21 -0
  30. package/android/src/main/java/com/omniretail/omnipay/FaceVerificationFrameProcessor.kt +0 -111
  31. package/ios/FaceVerificationFrameProcessor.swift +0 -138
  32. package/ios/FaceVerificationFrameProcessorPlugin.m +0 -4
  33. package/ios/OmnipayReactnativeSdk.m +0 -5
  34. package/ios/OmnipayReactnativeSdk.swift +0 -10
  35. package/ios/omnipay-reactnative-sdk-Bridging-Header.h +0 -8
  36. package/ios/omnipay_reactnative_sdk.h +0 -6
  37. package/lib/commonjs/components/Button.js +0 -68
  38. package/lib/commonjs/components/Button.js.map +0 -1
  39. package/lib/commonjs/components/biometrics/FaceVerification.js +0 -380
  40. package/lib/commonjs/components/biometrics/FaceVerification.js.map +0 -1
  41. package/lib/commonjs/components/biometrics/useFaceVerification.js +0 -85
  42. package/lib/commonjs/components/biometrics/useFaceVerification.js.map +0 -1
  43. package/lib/commonjs/components/biometrics/useFaceVerificationFlow.js +0 -157
  44. package/lib/commonjs/components/biometrics/useFaceVerificationFlow.js.map +0 -1
  45. package/lib/module/components/Button.js +0 -61
  46. package/lib/module/components/Button.js.map +0 -1
  47. package/lib/module/components/biometrics/FaceVerification.js +0 -372
  48. package/lib/module/components/biometrics/FaceVerification.js.map +0 -1
  49. package/lib/module/components/biometrics/useFaceVerification.js +0 -78
  50. package/lib/module/components/biometrics/useFaceVerification.js.map +0 -1
  51. package/lib/module/components/biometrics/useFaceVerificationFlow.js +0 -150
  52. package/lib/module/components/biometrics/useFaceVerificationFlow.js.map +0 -1
  53. package/lib/typescript/components/Button.d.ts +0 -17
  54. package/lib/typescript/components/Button.d.ts.map +0 -1
  55. package/lib/typescript/components/biometrics/FaceVerification.d.ts +0 -9
  56. package/lib/typescript/components/biometrics/FaceVerification.d.ts.map +0 -1
  57. package/lib/typescript/components/biometrics/useFaceVerification.d.ts +0 -38
  58. package/lib/typescript/components/biometrics/useFaceVerification.d.ts.map +0 -1
  59. package/lib/typescript/components/biometrics/useFaceVerificationFlow.d.ts +0 -29
  60. package/lib/typescript/components/biometrics/useFaceVerificationFlow.d.ts.map +0 -1
  61. package/omnipay_reactnative_sdk.podspec +0 -52
  62. package/src/components/Button.tsx +0 -86
  63. package/src/components/biometrics/FaceVerification.tsx +0 -429
  64. package/src/components/biometrics/useFaceVerification.ts +0 -120
  65. package/src/components/biometrics/useFaceVerificationFlow.ts +0 -224
@@ -12,12 +12,16 @@ import {
12
12
  Image,
13
13
  TouchableWithoutFeedback,
14
14
  Text,
15
+ NativeModules,
16
+ NativeEventEmitter,
15
17
  } from 'react-native';
16
- import WebView, { WebViewMessageEvent } from 'react-native-webview';
18
+ import WebView, { type WebViewMessageEvent } from 'react-native-webview';
17
19
  import { getContact } from '../functions';
18
20
  import Share from 'react-native-share';
19
- import FaceVerification from './biometrics/FaceVerification';
20
- import Button from './Button';
21
+ import AsyncStorage from '@react-native-async-storage/async-storage';
22
+ import buildUrlWithMetadata from 'src/utils/buildUrlWithMetadata';
23
+
24
+ const OmnipayActivity = NativeModules.OmnipayActivity || {};
21
25
 
22
26
  type OmnipayProviderProps = {
23
27
  publicKey: string;
@@ -26,6 +30,16 @@ type OmnipayProviderProps = {
26
30
  children: React.ReactElement | React.ReactElement[];
27
31
  };
28
32
 
33
+ type PosTransactionType = {
34
+ amount: number;
35
+ purchaseType: 'PURCHASE' | 'KEY EXCHANGE';
36
+ color: string;
37
+ print: boolean;
38
+ rrn: string;
39
+ stan: string;
40
+ terminalId: string;
41
+ };
42
+
29
43
  type PostMessage = {
30
44
  [key: string]: unknown;
31
45
  };
@@ -35,6 +49,7 @@ type Status = 'error' | 'loading' | 'success';
35
49
  type InitiateBillsType = {
36
50
  phoneNumber: string;
37
51
  onClose?: () => void;
52
+ metadata?: Record<string, string | number | boolean>;
38
53
  };
39
54
 
40
55
  type InitiateWalletType = {
@@ -57,6 +72,8 @@ type InitiateWalletType = {
57
72
  sessionId?: string;
58
73
  kycStatus?: 'verified' | 'unverified';
59
74
  launchPage?: string;
75
+ promoName?: string;
76
+ metadata?: Record<string, string | number | boolean>;
60
77
  };
61
78
 
62
79
  export type OmnipayContextType = {
@@ -81,6 +98,8 @@ export type OmnipayContextType = {
81
98
  sessionId,
82
99
  launchPage,
83
100
  kycStatus,
101
+ promoName,
102
+ metadata,
84
103
  }: InitiateWalletType) => void;
85
104
  };
86
105
 
@@ -110,18 +129,99 @@ export const OmnipayProvider = ({
110
129
  const isValidEnv = ['prod', 'dev'].includes(env);
111
130
  const isValidColor = color.length > 2;
112
131
  const onCloseRef = useRef<(() => void) | undefined>(undefined);
113
- const [showFaceVerification, setShowFaceVerification] = useState(false);
132
+ const [canUsePos, setCanUsePos] = useState(false);
114
133
 
115
134
  useEffect(() => {
116
- setTimeout(() => {
117
- setShowFaceVerification(true);
118
- }, 1000);
135
+ checkPaymentApp();
119
136
  }, []);
120
137
 
121
138
  useEffect(() => {
122
139
  visibilityRef.current = isVisible;
123
140
  }, [isVisible]);
124
141
 
142
+ useEffect(() => {
143
+ if (canUsePos) {
144
+ const eventEmitter = new NativeEventEmitter(OmnipayActivity);
145
+ eventEmitter.addListener('OmnipayEvent', (event) => {
146
+ console.log('native event', event);
147
+ });
148
+ }
149
+ }, [canUsePos]);
150
+
151
+ async function checkPaymentApp() {
152
+ try {
153
+ if (Platform.OS === 'android') {
154
+ const isInstalled = await OmnipayActivity.isPackageInstalled(
155
+ 'com.horizonpay.sample'
156
+ );
157
+ if (isInstalled) {
158
+ setCanUsePos(true);
159
+ }
160
+ }
161
+ } catch (error) {}
162
+ }
163
+
164
+ async function startPosTransaction({
165
+ amount,
166
+ purchaseType,
167
+ print,
168
+ rrn,
169
+ stan,
170
+ terminalId,
171
+ }: PosTransactionType) {
172
+ try {
173
+ if (Platform.OS === 'android') {
174
+ let result = '';
175
+
176
+ if (purchaseType === 'KEY EXCHANGE') {
177
+ const isKeyExchanged = await AsyncStorage.getItem('isKeyExchanged');
178
+ if (!isKeyExchanged) {
179
+ result = await OmnipayActivity.initiateHorizonTransaction(
180
+ amount,
181
+ purchaseType,
182
+ color,
183
+ print,
184
+ rrn,
185
+ stan,
186
+ terminalId
187
+ );
188
+ if (
189
+ terminalId &&
190
+ result &&
191
+ result.toLowerCase().includes('-message-success')
192
+ ) {
193
+ await AsyncStorage.setItem('isKeyExchanged', terminalId);
194
+ }
195
+ postMessage({
196
+ dataKey: 'onPosKeyExchanged',
197
+ dataValue: result,
198
+ });
199
+ }
200
+ } else {
201
+ result = await OmnipayActivity.initiateHorizonTransaction(
202
+ amount,
203
+ purchaseType,
204
+ color,
205
+ print,
206
+ rrn,
207
+ stan,
208
+ terminalId
209
+ );
210
+ postMessage({
211
+ dataKey: 'onPosTransactionSuccess',
212
+ dataValue: result,
213
+ });
214
+ }
215
+ }
216
+ } catch (error) {
217
+ console.log(error);
218
+ postMessage({
219
+ dataKey: 'onPosTransactionFailure',
220
+ dataValue: '',
221
+ });
222
+ }
223
+ }
224
+
125
225
  function getWebviewStyle() {
126
226
  if (!showWebview) {
127
227
  return { opacity: 0, height: 0, width: 0, flex: 0 };
@@ -181,6 +281,9 @@ export const OmnipayProvider = ({
181
281
  if (dataKey === 'shareReceipt') {
182
282
  shareReceipt(dataValue);
183
283
  }
284
+ if (dataKey === 'startPosTransaction') {
285
+ startPosTransaction(JSON.parse(dataValue));
286
+ }
184
287
  }
185
288
  } catch (error) {}
186
289
  }
@@ -197,16 +300,21 @@ export const OmnipayProvider = ({
197
300
  }
198
301
  }
199
302
 
200
- const _initiateBills = ({ phoneNumber, onClose }: InitiateBillsType) => {
201
- if (typeof phoneNumber === 'string' && phoneNumber.length >= 10) {
202
- const webUrl = `${webHost}?theme=${color}&view=bills&publicKey=${publicKey}&phoneNumber=${phoneNumber}`;
203
- setWebviewUrl(webUrl);
204
- setIsVisible(true);
205
- onCloseRef.current = onClose;
206
- return;
207
- }
208
- console.warn('Omnipay error: Invalid phone number');
209
- };
303
+ const _initiateBills = ({
304
+ phoneNumber,
305
+ onClose,
306
+ metadata = {},
307
+ }: InitiateBillsType) => {
308
+ if (typeof phoneNumber === 'string' && phoneNumber.length >= 10) {
309
+ const baseUrl = `${webHost}?theme=${color}&view=bills&publicKey=${publicKey}&phoneNumber=${phoneNumber}`;
310
+ const webUrl = buildUrlWithMetadata(baseUrl, metadata);
311
+ setWebviewUrl(webUrl);
312
+ setIsVisible(true);
313
+ onCloseRef.current = onClose;
314
+ return;
315
+ }
316
+ console.warn('Omnipay error: Invalid phone number');
317
+ };
210
318
 
211
319
  const _initiateWallet = ({
212
320
  phoneNumber = '',
@@ -228,6 +336,8 @@ export const OmnipayProvider = ({
228
336
  sessionId = '',
229
337
  kycStatus,
230
338
  launchPage = 'wallet',
339
+ promoName = '',
340
+ metadata = {},
231
341
  }: InitiateWalletType) => {
232
342
  //prevent opening if it's already open
233
343
  if (visibilityRef.current) {
@@ -239,9 +349,10 @@ export const OmnipayProvider = ({
239
349
  const usesNativeShare = true;
240
350
 
241
351
  if (isPhoneNumberValid || isValidCustomerRef || isValidUserRef) {
242
- const webUrl = `${webHost}?theme=${color}&view=wallet&publicKey=${publicKey}&phoneNumber=${phoneNumber}&usesPaylater=${usesPaylater}&usesPromo=${usesPromo}&usesAirtimeData=${usesAirtimeData}&usesTransfer=${usesTransfer}&usesBills=${usesBills}&usesPos=${usesPos}&customerRef=${customerRef}&userRef=${userRef}&promoBalanceOffset=${promoBalanceOffset}&deviceId=${deviceId}&deviceName=${deviceName}&hideWalletTransfer=${hideWalletTransfer}&bvnRequired=${isBvnValidationRequired}&usesNativeShare=${usesNativeShare}&walletTab=${walletTab}&sessionId=${sessionId}&kycStatus=${
352
+ const baseUrl = `${webHost}?theme=${color}&view=wallet&publicKey=${publicKey}&phoneNumber=${phoneNumber}&usesPaylater=${usesPaylater}&usesPromo=${usesPromo}&usesAirtimeData=${usesAirtimeData}&usesTransfer=${usesTransfer}&usesBills=${usesBills}&usesPos=${usesPos}&customerRef=${customerRef}&userRef=${userRef}&promoBalanceOffset=${promoBalanceOffset}&deviceId=${deviceId}&deviceName=${deviceName}&hideWalletTransfer=${hideWalletTransfer}&bvnRequired=${isBvnValidationRequired}&usesNativeShare=${usesNativeShare}&isPosEnabled=${canUsePos}&walletTab=${walletTab}&sessionId=${sessionId}&kycStatus=${
243
353
  kycStatus || ''
244
- }&launchPage=${launchPage}`;
354
+ }&launchPage=${launchPage}&promoName=${promoName}`;
355
+ const webUrl = buildUrlWithMetadata(baseUrl, metadata);
245
356
  setWebviewUrl(webUrl);
246
357
  setIsVisible(true);
247
358
  onCloseRef.current = onClose;
@@ -317,6 +428,7 @@ export const OmnipayProvider = ({
317
428
  }}
318
429
  domStorageEnabled={true}
319
430
  originWhitelist={['*']}
431
+ allowsInlineMediaPlayback={true}
320
432
  onLoadEnd={() => setWebviewStatus('success')}
321
433
  renderError={() => (
322
434
  <View style={StyleSheet.absoluteFillObject}>
@@ -324,12 +436,21 @@ export const OmnipayProvider = ({
324
436
  <Text style={styles.errorSubtitle}>
325
437
  Unable to open your wallet. Please try again
326
438
  </Text>
327
- <Button
328
- title="Retry"
439
+ <TouchableOpacity
440
+ activeOpacity={0.8}
329
441
  onPress={reloadWebview}
330
- backgroundColor={color}
331
- borderColor={color}
332
- />
442
+ style={[
443
+ styles.button,
444
+ {
445
+ backgroundColor: color,
446
+ borderColor: color,
447
+ },
448
+ ]}
449
+ >
450
+ <>
451
+ <Text style={styles.buttonText}>Retry</Text>
452
+ </>
453
+ </TouchableOpacity>
333
454
  </View>
334
455
  </View>
335
456
  )}
@@ -350,13 +471,6 @@ export const OmnipayProvider = ({
350
471
  </>
351
472
  )}
352
473
  {children}
353
- {showFaceVerification && (
354
- <FaceVerification
355
- onClose={() => setShowFaceVerification(false)}
356
- onSuccess={() => setShowFaceVerification(false)}
357
- primaryColor={color}
358
- />
359
- )}
360
474
  </OmnipayContext.Provider>
361
475
  );
362
476
  };
@@ -467,4 +581,13 @@ const styles = StyleSheet.create({
467
581
  minWidth: 160,
468
582
  marginHorizontal: 'auto',
469
583
  },
584
+ button: {
585
+ borderRadius: 6,
586
+ paddingHorizontal: 12,
587
+ paddingVertical: 14,
588
+ borderWidth: 1,
589
+ alignItems: 'center',
590
+ justifyContent: 'center',
591
+ },
592
+ buttonText: { color: 'white', fontSize: 16, paddingHorizontal: 30 },
470
593
  });
@@ -7,7 +7,7 @@ import {
7
7
  Text,
8
8
  Linking,
9
9
  } from 'react-native';
10
- import { WebView, WebViewMessageEvent } from 'react-native-webview';
10
+ import { WebView, type WebViewMessageEvent } from 'react-native-webview';
11
11
  import { clientSdkBaseUrl, serverSdkBaseUrl } from '../lib/config';
12
12
  import { getContact } from '../functions';
13
13
  import { Registration } from './views/Registration';
@@ -6,7 +6,7 @@ import {
6
6
  Text,
7
7
  View,
8
8
  } from 'react-native';
9
- import { WebView, WebViewMessageEvent } from 'react-native-webview';
9
+ import { WebView, type WebViewMessageEvent } from 'react-native-webview';
10
10
  import { serverSdkBaseUrl } from '../../lib/config';
11
11
 
12
12
  type OmnipayProps = {
@@ -6,7 +6,7 @@ import {
6
6
  Text,
7
7
  View,
8
8
  } from 'react-native';
9
- import { WebView, WebViewMessageEvent } from 'react-native-webview';
9
+ import { WebView, type WebViewMessageEvent } from 'react-native-webview';
10
10
  import { serverSdkBaseUrl } from '../../lib/config';
11
11
 
12
12
  type AgreementSubmittedType = {
@@ -6,7 +6,7 @@ import {
6
6
  Text,
7
7
  View,
8
8
  } from 'react-native';
9
- import { WebView, WebViewMessageEvent } from 'react-native-webview';
9
+ import { WebView, type WebViewMessageEvent } from 'react-native-webview';
10
10
  import { serverSdkBaseUrl } from '../../lib/config';
11
11
 
12
12
  type RegisterSuccessType = {
@@ -1,7 +1,7 @@
1
1
  import { useContext } from 'react';
2
2
  import {
3
3
  OmnipayContext,
4
- OmnipayContextType,
4
+ type OmnipayContextType,
5
5
  } from '../components/OmnipayProvider';
6
6
 
7
7
  export function useOmnipay() {
@@ -0,0 +1,21 @@
1
+ export default function buildUrlWithMetadata(
2
+ baseUrl: string,
3
+ metadata: Record<string, string | number | boolean>
4
+ ): string {
5
+ const query = Object.entries(metadata)
6
+ .filter(
7
+ ([_, value]) => value !== undefined && value !== null && value !== ''
8
+ )
9
+ .map(
10
+ ([key, value]) =>
11
+ `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
12
+ )
13
+ .join('&');
14
+ if (query) {
15
+ return baseUrl.includes('?')
16
+ ? `${baseUrl}&${query}`
17
+ : `${baseUrl}?${query}`;
18
+ }
19
+
20
+ return baseUrl;
21
+ }
@@ -1,111 +0,0 @@
1
- package com.omniretail.omnipay
2
-
3
- import android.util.Log
4
- import com.google.mlkit.vision.common.InputImage
5
- import com.google.mlkit.vision.face.FaceDetection
6
- import com.google.mlkit.vision.face.FaceDetectorOptions
7
- import com.google.mlkit.vision.face.Face
8
- import com.mrousavy.camera.core.FrameInvalidError
9
- import com.mrousavy.camera.frameprocessor.Frame
10
- import com.mrousavy.camera.frameprocessor.FrameProcessorPlugin
11
- import com.mrousavy.camera.frameprocessor.VisionCameraProxy
12
-
13
- class FaceVerificationFrameProcessor(proxy: VisionCameraProxy, options: Map<String, Any>?) : FrameProcessorPlugin() {
14
-
15
- private val faceDetector = FaceDetection.getClient(
16
- FaceDetectorOptions.Builder()
17
- .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
18
- .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
19
- .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
20
- .setMinFaceSize(0.15f)
21
- .enableTracking()
22
- .build()
23
- )
24
-
25
- override fun callback(frame: Frame, arguments: Map<String, Any>?): Any {
26
- return try {
27
- val results = mutableMapOf<String, Any>()
28
-
29
- // Convert frame to InputImage
30
- val image = InputImage.fromMediaImage(frame.image, frame.imageRotationDegrees)
31
-
32
- // Process face detection synchronously
33
- val task = faceDetector.process(image)
34
-
35
- // Wait for result (this blocks the thread but that's OK for frame processors)
36
- val faces = try {
37
- // For simplicity, we'll use a blocking approach
38
- // In production, you might want to handle this differently
39
- while (!task.isComplete && !task.isCanceled) {
40
- Thread.sleep(1)
41
- }
42
-
43
- if (task.isSuccessful) {
44
- task.result
45
- } else {
46
- emptyList()
47
- }
48
- } catch (e: Exception) {
49
- Log.e("FaceVerification", "Face detection failed: ${e.message}")
50
- results["error"] = e.message ?: "Face detection failed"
51
- return results
52
- }
53
-
54
- if (faces.isEmpty()) {
55
- results["faceDetected"] = false
56
- return results
57
- }
58
-
59
- // Process first detected face
60
- val face = faces[0]
61
- results["faceDetected"] = true
62
-
63
- // Face bounding box
64
- results["boundingBox"] = mapOf(
65
- "left" to face.boundingBox.left,
66
- "top" to face.boundingBox.top,
67
- "right" to face.boundingBox.right,
68
- "bottom" to face.boundingBox.bottom
69
- )
70
-
71
- // Smile detection
72
- face.smilingProbability?.let { smilingProbability ->
73
- results["isSmiling"] = smilingProbability > 0.7f
74
- results["smileProbability"] = smilingProbability
75
- }
76
-
77
- // Eye detection (for blinking)
78
- val leftEyeOpenProbability = face.leftEyeOpenProbability
79
- val rightEyeOpenProbability = face.rightEyeOpenProbability
80
-
81
- if (leftEyeOpenProbability != null && rightEyeOpenProbability != null) {
82
- val leftEyeClosed = leftEyeOpenProbability < 0.3f
83
- val rightEyeClosed = rightEyeOpenProbability < 0.3f
84
-
85
- results["leftEyeClosed"] = leftEyeClosed
86
- results["rightEyeClosed"] = rightEyeClosed
87
- results["isBlinking"] = leftEyeClosed && rightEyeClosed
88
- results["leftEyeOpenProbability"] = leftEyeOpenProbability
89
- results["rightEyeOpenProbability"] = rightEyeOpenProbability
90
- }
91
-
92
- // Head pose detection
93
- results["headPose"] = mapOf(
94
- "yaw" to face.headEulerAngleY, // Left-right movement
95
- "pitch" to face.headEulerAngleX, // Up-down movement
96
- "roll" to face.headEulerAngleZ // Tilt movement
97
- )
98
-
99
- // Face tracking ID (useful for consistency across frames)
100
- face.trackingId?.let { trackingId ->
101
- results["trackingId"] = trackingId
102
- }
103
-
104
- results
105
-
106
- } catch (e: Exception) {
107
- Log.e("FaceVerification", "Unexpected error: ${e.message}")
108
- mapOf("error" to (e.message ?: "Unknown error occurred"))
109
- }
110
- }
111
- }
@@ -1,138 +0,0 @@
1
- import VisionCamera
2
- import Vision
3
- import AVFoundation
4
-
5
- @objc(FaceVerificationFrameProcessor)
6
- public class FaceVerificationFrameProcessor: FrameProcessorPlugin {
7
-
8
- public override init(proxy: VisionCameraProxyHolder, options: [AnyHashable : Any]! = [:]) {
9
- super.init(proxy: proxy, options: options)
10
- }
11
-
12
- public override func callback(_ frame: Frame, withArguments arguments: [AnyHashable : Any]?) -> Any {
13
- let buffer = frame.buffer
14
- guard let imageBuffer = CMSampleBufferGetImageBuffer(buffer) else {
15
- return ["error": "Failed to get image buffer"]
16
- }
17
-
18
- return performFaceDetection(on: imageBuffer)
19
- }
20
-
21
- private func performFaceDetection(on imageBuffer: CVImageBuffer) -> [String: Any] {
22
- let request = VNDetectFaceLandmarksRequest()
23
- let handler = VNImageRequestHandler(cvPixelBuffer: imageBuffer, options: [:])
24
-
25
- do {
26
- try handler.perform([request])
27
-
28
- guard let observations = request.results as? [VNFaceObservation],
29
- let face = observations.first else {
30
- return ["faceDetected": false]
31
- }
32
-
33
- var results: [String: Any] = [
34
- "faceDetected": true,
35
- "boundingBox": [
36
- "x": face.boundingBox.origin.x,
37
- "y": face.boundingBox.origin.y,
38
- "width": face.boundingBox.size.width,
39
- "height": face.boundingBox.size.height
40
- ]
41
- ]
42
-
43
- // Analyze facial features
44
- if let landmarks = face.landmarks {
45
- results = analyzeFacialFeatures(landmarks: landmarks, results: results)
46
- }
47
-
48
- // Calculate head pose
49
- if let pose = calculateHeadPose(face: face) {
50
- results["headPose"] = pose
51
- }
52
-
53
- return results
54
-
55
- } catch {
56
- return ["error": error.localizedDescription]
57
- }
58
- }
59
-
60
- private func analyzeFacialFeatures(landmarks: VNFaceLandmarks2D, results: [String: Any]) -> [String: Any] {
61
- var updatedResults = results
62
-
63
- // Detect smile
64
- if let mouth = landmarks.outerLips {
65
- let isSmiling = detectSmile(mouthPoints: mouth.normalizedPoints)
66
- updatedResults["isSmiling"] = isSmiling
67
- }
68
-
69
- // Detect blinks
70
- if let leftEye = landmarks.leftEye, let rightEye = landmarks.rightEye {
71
- let leftEyeClosed = detectEyeClosure(eyePoints: leftEye.normalizedPoints)
72
- let rightEyeClosed = detectEyeClosure(eyePoints: rightEye.normalizedPoints)
73
-
74
- updatedResults["leftEyeClosed"] = leftEyeClosed
75
- updatedResults["rightEyeClosed"] = rightEyeClosed
76
- updatedResults["isBlinking"] = leftEyeClosed && rightEyeClosed
77
- }
78
-
79
- return updatedResults
80
- }
81
-
82
- private func detectSmile(mouthPoints: [CGPoint]) -> Bool {
83
- guard mouthPoints.count >= 6 else { return false }
84
-
85
- // Calculate mouth corner heights vs center
86
- let leftCorner = mouthPoints[0]
87
- let rightCorner = mouthPoints[3]
88
- let topCenter = mouthPoints[1]
89
- let bottomCenter = mouthPoints[4]
90
-
91
- let cornerHeight = (leftCorner.y + rightCorner.y) / 2
92
- let centerHeight = (topCenter.y + bottomCenter.y) / 2
93
-
94
- // Smile detection: corners higher than center
95
- return cornerHeight < centerHeight - 0.01
96
- }
97
-
98
- private func detectEyeClosure(eyePoints: [CGPoint]) -> Bool {
99
- guard eyePoints.count >= 6 else { return false }
100
-
101
- // Calculate eye aspect ratio
102
- let topPoints = Array(eyePoints[1...2])
103
- let bottomPoints = Array(eyePoints[4...5])
104
- let leftPoint = eyePoints[0]
105
- let rightPoint = eyePoints[3]
106
-
107
- let verticalDist1 = distance(topPoints[0], bottomPoints[0])
108
- let verticalDist2 = distance(topPoints[1], bottomPoints[1])
109
- let horizontalDist = distance(leftPoint, rightPoint)
110
-
111
- let eyeAspectRatio = (verticalDist1 + verticalDist2) / (2.0 * horizontalDist)
112
-
113
- // Eye is closed if aspect ratio is below threshold
114
- return eyeAspectRatio < 0.2
115
- }
116
-
117
- private func calculateHeadPose(face: VNFaceObservation) -> [String: Double]? {
118
- guard let yaw = face.yaw, let pitch = face.pitch, let roll = face.roll else {
119
- return nil
120
- }
121
-
122
- let yawDegrees = Double(truncating: yaw) * 180.0 / Double.pi
123
- let pitchDegrees = Double(truncating: pitch) * 180.0 / Double.pi
124
- let rollDegrees = Double(truncating: roll) * 180.0 / Double.pi
125
-
126
- return [
127
- "yaw": yawDegrees, // Left-right head movement
128
- "pitch": pitchDegrees, // Up-down head movement
129
- "roll": rollDegrees // Head tilt
130
- ]
131
- }
132
-
133
- private func distance(_ point1: CGPoint, _ point2: CGPoint) -> Double {
134
- let dx = point1.x - point2.x
135
- let dy = point1.y - point2.y
136
- return sqrt(Double(dx * dx + dy * dy))
137
- }
138
- }
@@ -1,4 +0,0 @@
1
- #import <VisionCamera/FrameProcessorPlugin.h>
2
- #import <VisionCamera/FrameProcessorPluginRegistry.h>
3
-
4
- VISION_EXPORT_SWIFT_FRAME_PROCESSOR(FaceVerificationFrameProcessor, detectFaces)
@@ -1,5 +0,0 @@
1
- #import <React/RCTBridgeModule.h>
2
-
3
- @interface RCT_EXTERN_MODULE(OmnipayReactnativeSdk, NSObject)
4
-
5
- @end
@@ -1,10 +0,0 @@
1
- import Foundation
2
-
3
- @objc(OmnipayReactnativeSdk)
4
- class OmnipayReactnativeSdk: NSObject {
5
-
6
- @objc static func requiresMainQueueSetup() -> Bool {
7
- return false
8
- }
9
-
10
- }
@@ -1,8 +0,0 @@
1
- //
2
- // Use this file to import your target's public headers that you would like to expose to Swift.
3
- //
4
-
5
- #import <React/RCTBridgeModule.h>
6
- #import <VisionCamera/FrameProcessorPlugin.h>
7
- #import <VisionCamera/FrameProcessorPluginRegistry.h>
8
- #import <VisionCamera/Frame.h>
@@ -1,6 +0,0 @@
1
- #ifndef omnipay_reactnative_sdk_h
2
- #define omnipay_reactnative_sdk_h
3
-
4
- #import <React/RCTBridgeModule.h>
5
-
6
- #endif /* omnipay_reactnative_sdk_h */