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.
- package/README.md +14 -64
- package/android/build.gradle +4 -15
- package/android/src/main/java/com/omniretail/omnipay/OmnipayActivityPackage.java +0 -5
- package/lib/commonjs/components/OmnipayProvider.js +98 -20
- package/lib/commonjs/components/OmnipayProvider.js.map +1 -1
- package/lib/commonjs/utils/buildUrlWithMetadata.js +14 -0
- package/lib/commonjs/utils/buildUrlWithMetadata.js.map +1 -0
- package/lib/module/components/OmnipayProvider.js +99 -21
- package/lib/module/components/OmnipayProvider.js.map +1 -1
- package/lib/module/components/OmnipayView.js.map +1 -1
- package/lib/module/components/views/BvnVerification.js.map +1 -1
- package/lib/module/components/views/PaylaterAgreement.js.map +1 -1
- package/lib/module/components/views/Registration.js.map +1 -1
- package/lib/module/utils/buildUrlWithMetadata.js +8 -0
- package/lib/module/utils/buildUrlWithMetadata.js.map +1 -0
- package/lib/typescript/components/OmnipayProvider.d.ts +5 -2
- package/lib/typescript/components/OmnipayProvider.d.ts.map +1 -1
- package/lib/typescript/hooks/useOmnipay.d.ts +4 -1
- package/lib/typescript/hooks/useOmnipay.d.ts.map +1 -1
- package/lib/typescript/utils/buildUrlWithMetadata.d.ts +2 -0
- package/lib/typescript/utils/buildUrlWithMetadata.d.ts.map +1 -0
- package/package.json +12 -29
- package/src/components/OmnipayProvider.tsx +154 -31
- package/src/components/OmnipayView.tsx +1 -1
- package/src/components/views/BvnVerification.tsx +1 -1
- package/src/components/views/PaylaterAgreement.tsx +1 -1
- package/src/components/views/Registration.tsx +1 -1
- package/src/hooks/useOmnipay.tsx +1 -1
- package/src/utils/buildUrlWithMetadata.ts +21 -0
- package/android/src/main/java/com/omniretail/omnipay/FaceVerificationFrameProcessor.kt +0 -111
- package/ios/FaceVerificationFrameProcessor.swift +0 -138
- package/ios/FaceVerificationFrameProcessorPlugin.m +0 -4
- package/ios/OmnipayReactnativeSdk.m +0 -5
- package/ios/OmnipayReactnativeSdk.swift +0 -10
- package/ios/omnipay-reactnative-sdk-Bridging-Header.h +0 -8
- package/ios/omnipay_reactnative_sdk.h +0 -6
- package/lib/commonjs/components/Button.js +0 -68
- package/lib/commonjs/components/Button.js.map +0 -1
- package/lib/commonjs/components/biometrics/FaceVerification.js +0 -380
- package/lib/commonjs/components/biometrics/FaceVerification.js.map +0 -1
- package/lib/commonjs/components/biometrics/useFaceVerification.js +0 -85
- package/lib/commonjs/components/biometrics/useFaceVerification.js.map +0 -1
- package/lib/commonjs/components/biometrics/useFaceVerificationFlow.js +0 -157
- package/lib/commonjs/components/biometrics/useFaceVerificationFlow.js.map +0 -1
- package/lib/module/components/Button.js +0 -61
- package/lib/module/components/Button.js.map +0 -1
- package/lib/module/components/biometrics/FaceVerification.js +0 -372
- package/lib/module/components/biometrics/FaceVerification.js.map +0 -1
- package/lib/module/components/biometrics/useFaceVerification.js +0 -78
- package/lib/module/components/biometrics/useFaceVerification.js.map +0 -1
- package/lib/module/components/biometrics/useFaceVerificationFlow.js +0 -150
- package/lib/module/components/biometrics/useFaceVerificationFlow.js.map +0 -1
- package/lib/typescript/components/Button.d.ts +0 -17
- package/lib/typescript/components/Button.d.ts.map +0 -1
- package/lib/typescript/components/biometrics/FaceVerification.d.ts +0 -9
- package/lib/typescript/components/biometrics/FaceVerification.d.ts.map +0 -1
- package/lib/typescript/components/biometrics/useFaceVerification.d.ts +0 -38
- package/lib/typescript/components/biometrics/useFaceVerification.d.ts.map +0 -1
- package/lib/typescript/components/biometrics/useFaceVerificationFlow.d.ts +0 -29
- package/lib/typescript/components/biometrics/useFaceVerificationFlow.d.ts.map +0 -1
- package/omnipay_reactnative_sdk.podspec +0 -52
- package/src/components/Button.tsx +0 -86
- package/src/components/biometrics/FaceVerification.tsx +0 -429
- package/src/components/biometrics/useFaceVerification.ts +0 -120
- 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
|
|
20
|
-
import
|
|
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 [
|
|
132
|
+
const [canUsePos, setCanUsePos] = useState(false);
|
|
114
133
|
|
|
115
134
|
useEffect(() => {
|
|
116
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
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
|
-
<
|
|
328
|
-
|
|
439
|
+
<TouchableOpacity
|
|
440
|
+
activeOpacity={0.8}
|
|
329
441
|
onPress={reloadWebview}
|
|
330
|
-
|
|
331
|
-
|
|
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 = {
|
package/src/hooks/useOmnipay.tsx
CHANGED
|
@@ -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,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>
|