@sanctum-key/react-native-sdk 1.0.24 → 1.0.25
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/build/package.json +3 -1
- package/build/src/components/EnhancedCameraView.d.ts +7 -1
- package/build/src/components/EnhancedCameraView.d.ts.map +1 -1
- package/build/src/components/EnhancedCameraView.js +88 -30
- package/build/src/components/EnhancedCameraView.js.map +1 -1
- package/build/src/components/EnhancedCameraView.web.d.ts +5 -1
- package/build/src/components/EnhancedCameraView.web.d.ts.map +1 -1
- package/build/src/components/EnhancedCameraView.web.js +80 -295
- package/build/src/components/EnhancedCameraView.web.js.map +1 -1
- package/build/src/components/KYCElements/CameraCapture.js +1 -1
- package/build/src/components/KYCElements/CameraCapture.js.map +1 -1
- package/build/src/components/KYCElements/CountrySelection.d.ts.map +1 -1
- package/build/src/components/KYCElements/CountrySelection.js +38 -218
- package/build/src/components/KYCElements/CountrySelection.js.map +1 -1
- package/build/src/components/KYCElements/CountrySelectionTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/CountrySelectionTemplate.js +5 -17
- package/build/src/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +509 -95
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.js +93 -51
- package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
- package/build/src/components/OverLay/type.d.ts +1 -0
- package/build/src/components/OverLay/type.d.ts.map +1 -1
- package/build/src/components/OverLay/type.js.map +1 -1
- package/build/src/components/WebBarcodeScanner.d.ts +9 -0
- package/build/src/components/WebBarcodeScanner.d.ts.map +1 -0
- package/build/src/components/WebBarcodeScanner.js +152 -0
- package/build/src/components/WebBarcodeScanner.js.map +1 -0
- package/build/src/config/KYCConfigs.js +2 -2
- package/build/src/config/KYCConfigs.js.map +1 -1
- package/build/src/config/region_mapping.json +0 -14
- package/build/src/modules/api/CardAuthentification.d.ts +2 -0
- package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/src/modules/api/CardAuthentification.js +22 -39
- package/build/src/modules/api/CardAuthentification.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts +7 -2
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +163 -57
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/build/src/types/KYC.types.d.ts +1 -0
- package/build/src/types/KYC.types.d.ts.map +1 -1
- package/build/src/types/KYC.types.js.map +1 -1
- package/build/src/utils/aamvaParser.d.ts +62 -0
- package/build/src/utils/aamvaParser.d.ts.map +1 -0
- package/build/src/utils/aamvaParser.js +91 -0
- package/build/src/utils/aamvaParser.js.map +1 -0
- package/build/src/utils/cropByObb.d.ts.map +1 -1
- package/build/src/utils/cropByObb.js +4 -3
- package/build/src/utils/cropByObb.js.map +1 -1
- package/build/src/web/WebKYCEntry.d.ts +1 -1
- package/build/src/web/WebKYCEntry.d.ts.map +1 -1
- package/build/src/web/WebKYCEntry.js +105 -239
- package/build/src/web/WebKYCEntry.js.map +1 -1
- package/package.json +3 -1
- package/src/components/EnhancedCameraView.tsx +116 -42
- package/src/components/EnhancedCameraView.web.tsx +98 -338
- package/src/components/KYCElements/CameraCapture.tsx +1 -1
- package/src/components/KYCElements/CountrySelection.tsx +60 -240
- package/src/components/KYCElements/CountrySelectionTemplate.tsx +7 -20
- package/src/components/KYCElements/IDCardCapture.tsx +672 -151
- package/src/components/KYCElements/VerificationProgressTemplate.tsx +112 -65
- package/src/components/OverLay/type.ts +1 -1
- package/src/components/WebBarcodeScanner.tsx +273 -0
- package/src/config/KYCConfigs.ts +2 -2
- package/src/config/region_mapping.json +0 -14
- package/src/modules/api/CardAuthentification.ts +41 -56
- package/src/modules/api/KYCService.ts +236 -82
- package/src/types/KYC.types.ts +1 -0
- package/src/utils/aamvaParser.ts +102 -0
- package/src/utils/cropByObb.ts +5 -3
- package/src/web/WebKYCEntry.tsx +125 -301
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react';
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
|
|
3
3
|
import { TemplateComponent } from '../../types/KYC.types';
|
|
4
4
|
import { useTemplateKYCFlowContext } from '../../hooks/useTemplateKYCFlow';
|
|
@@ -20,85 +20,139 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
20
20
|
const [retryCount, setRetryCount] = useState(0);
|
|
21
21
|
const [isRetrying, setIsRetrying] = useState(false);
|
|
22
22
|
const [timeoutId, setTimeoutId] = useState<number | null>(null);
|
|
23
|
+
const isPollingRef = useRef(false);
|
|
23
24
|
|
|
24
25
|
const verification = state.verification;
|
|
25
26
|
const isSuccess = verification.status === 'success';
|
|
26
27
|
const isFailed = verification.status === 'failed';
|
|
27
28
|
|
|
28
|
-
const getVerificationResult = async (currentRetryCount = 0, maxRetries =
|
|
29
|
+
const getVerificationResult = async (currentRetryCount = 0, maxRetries = 60) => {
|
|
29
30
|
try {
|
|
30
31
|
logger.log(`Starting verification check (attempt ${currentRetryCount + 1}/${maxRetries + 1})`);
|
|
31
32
|
const result: any = await kycService.getVerificationResult(state.session.session_id);
|
|
32
33
|
|
|
33
34
|
logger.log('getVerificationResult raw result:', JSON.stringify(result, null, 2));
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
const rootStatus = (result?.status || '').toLowerCase();
|
|
37
|
+
if (rootStatus === 'failed.' || rootStatus === 'failed' || rootStatus === 'rejected' || rootStatus === 'error') {
|
|
36
38
|
actions.setVerificationState({
|
|
37
39
|
status: 'failed',
|
|
38
40
|
result: {
|
|
39
41
|
referenceId: state.session.session_id,
|
|
40
|
-
issues: [result?.message || 'Verification failed. Sent for manual review.']
|
|
42
|
+
issues: [result?.message || result?.reason || 'Verification failed. Sent for manual review.']
|
|
41
43
|
} as any
|
|
42
44
|
});
|
|
43
45
|
setIsRetrying(false);
|
|
44
|
-
|
|
46
|
+
isPollingRef.current = false; // Release lock
|
|
47
|
+
return;
|
|
45
48
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
|
|
50
|
+
// 2. Safely Extract Nested Payload
|
|
51
|
+
let vData = result?.data || result; // Fallback to root if .data is missing
|
|
52
|
+
|
|
53
|
+
// Handle stringified JSON
|
|
54
|
+
if (typeof vData === 'string') {
|
|
55
|
+
try {
|
|
56
|
+
vData = JSON.parse(vData);
|
|
57
|
+
} catch (e) { }
|
|
52
58
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
|
|
60
|
+
// Handle backend dictionaries indexed by session_id
|
|
61
|
+
if (vData && typeof vData === 'object' && vData[state.session.session_id]) {
|
|
62
|
+
let nested = vData[state.session.session_id];
|
|
63
|
+
if (nested && nested.data) {
|
|
64
|
+
nested = typeof nested.data === 'string' ? JSON.parse(nested.data) : nested.data;
|
|
65
|
+
}
|
|
66
|
+
vData = nested;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!vData || typeof vData !== 'object') {
|
|
70
|
+
throw new Error('Verification payload missing or invalid for current session');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const vStatus = (vData.verification_status || vData.status || vData.state || '').toLowerCase();
|
|
74
|
+
const vDecision = (vData.decision || vData.outcome || vData.result || '').toLowerCase();
|
|
75
|
+
// const processingError = vData.reason || vData.error || vData.issues || vData.error_message;
|
|
76
|
+
const selfieMatch = vData.selfie_info?.is_match; // true, false, or null
|
|
77
|
+
|
|
78
|
+
logger.log(`Parsed Status: [${vStatus}], Decision: [${vDecision}], Selfie Match: [${selfieMatch}]`);
|
|
79
|
+
|
|
80
|
+
// 4. Evaluate Failures First
|
|
81
|
+
// 5. Evaluate Definitive Success
|
|
82
|
+
if (
|
|
83
|
+
vStatus === 'approved' || vStatus === 'verified' || vStatus === 'success' ||
|
|
84
|
+
vDecision === 'approved' || vDecision === 'passed' || vDecision === 'clear' ||
|
|
85
|
+
(vStatus === 'completed' && selfieMatch === true)
|
|
86
|
+
) {
|
|
87
|
+
|
|
88
|
+
// 🚨 THE FIX: Prevent False Positives (Ghost Payloads)
|
|
89
|
+
// If it says success, but the data object is completely empty, it is actually a failure.
|
|
90
|
+
const hasActualData = vData && Object.keys(vData).length > 0;
|
|
91
|
+
|
|
92
|
+
if (!hasActualData) {
|
|
93
|
+
logger.warn('Backend returned success, but data payload is entirely empty.');
|
|
94
|
+
actions.setVerificationState({
|
|
95
|
+
status: 'failed',
|
|
96
|
+
result: {
|
|
97
|
+
referenceId: state.session.session_id,
|
|
98
|
+
issues: ['Document text could not be read clearly. Please retake the photo in better lighting.']
|
|
99
|
+
} as any
|
|
100
|
+
});
|
|
101
|
+
setIsRetrying(false);
|
|
102
|
+
isPollingRef.current = false;
|
|
103
|
+
return;
|
|
56
104
|
}
|
|
57
105
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
106
|
+
// If we have data, proceed to actual success
|
|
107
|
+
actions.setVerificationState({
|
|
108
|
+
status: 'success',
|
|
109
|
+
result: {
|
|
110
|
+
...result,
|
|
111
|
+
referenceId: vData.verification_id || vData.session_id || state.session.session_id
|
|
112
|
+
} as any
|
|
113
|
+
});
|
|
114
|
+
setIsRetrying(false);
|
|
115
|
+
isPollingRef.current = false;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
vStatus === 'processing' || vStatus === 'pending' || vStatus === 'in_progress' ||
|
|
121
|
+
vStatus === 'initiated' || (vStatus === 'completed' && selfieMatch === null) || !vStatus
|
|
122
|
+
) {
|
|
61
123
|
if (currentRetryCount < maxRetries) {
|
|
62
124
|
const nextRetryCount = currentRetryCount + 1;
|
|
63
125
|
setRetryCount(nextRetryCount);
|
|
64
126
|
setIsRetrying(true);
|
|
65
|
-
logger.log(`Verification still processing, retrying in 10s... (${nextRetryCount}/${maxRetries})`);
|
|
66
127
|
|
|
67
|
-
|
|
128
|
+
logger.log(`Verification still processing, retrying in 3s... (${nextRetryCount}/${maxRetries})`);
|
|
68
129
|
|
|
130
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
69
131
|
const newTimeoutId = setTimeout(() => {
|
|
70
132
|
getVerificationResult(nextRetryCount, maxRetries);
|
|
71
|
-
},
|
|
133
|
+
}, 3000);
|
|
72
134
|
|
|
73
135
|
setTimeoutId(newTimeoutId as any);
|
|
74
|
-
return;
|
|
75
136
|
} else {
|
|
76
137
|
logger.error('Max retries reached, verification still processing');
|
|
77
138
|
setIsRetrying(false);
|
|
78
|
-
|
|
139
|
+
isPollingRef.current = false;
|
|
140
|
+
actions.setVerificationState({
|
|
141
|
+
status: 'failed',
|
|
142
|
+
result: {
|
|
143
|
+
referenceId: state.session.session_id,
|
|
144
|
+
issues: ['Verification timed out. Please try again.']
|
|
145
|
+
} as any
|
|
146
|
+
});
|
|
79
147
|
}
|
|
148
|
+
return;
|
|
80
149
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
status: 'success',
|
|
85
|
-
result: {
|
|
86
|
-
...result,
|
|
87
|
-
referenceId: verificationResult.verification_id || verificationResult.session_id
|
|
88
|
-
} as any
|
|
89
|
-
});
|
|
90
|
-
} else if (currentStatus === "failed" || currentStatus === "rejected") {
|
|
91
|
-
actions.setVerificationState({
|
|
92
|
-
status: 'failed',
|
|
93
|
-
result: {
|
|
94
|
-
referenceId: verificationResult.verification_id || state.session.session_id,
|
|
95
|
-
issues: ['Verification failed during processing.']
|
|
96
|
-
} as any
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
150
|
+
|
|
151
|
+
// Fallback release if status is completely unknown
|
|
152
|
+
logger.warn(`Unknown verification status received: ${vStatus}`);
|
|
100
153
|
setIsRetrying(false);
|
|
101
|
-
|
|
154
|
+
isPollingRef.current = false;
|
|
155
|
+
|
|
102
156
|
} catch (error: any) {
|
|
103
157
|
logger.error('getVerificationResult error', JSON.stringify(errorMessage(error) || { message: error?.message || 'Unknown error' }, null, 2));
|
|
104
158
|
|
|
@@ -106,47 +160,45 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
106
160
|
const nextRetryCount = currentRetryCount + 1;
|
|
107
161
|
setRetryCount(nextRetryCount);
|
|
108
162
|
setIsRetrying(true);
|
|
109
|
-
logger.log(`getVerificationResult Error occurred, retrying in 10s... (${nextRetryCount}/${maxRetries})`);
|
|
110
163
|
|
|
111
|
-
|
|
164
|
+
logger.log(`getVerificationResult Error occurred, retrying in 3s... (${nextRetryCount}/${maxRetries})`);
|
|
112
165
|
|
|
166
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
113
167
|
const newTimeoutId = setTimeout(() => {
|
|
114
168
|
getVerificationResult(nextRetryCount, maxRetries);
|
|
115
|
-
},
|
|
169
|
+
}, 3000);
|
|
116
170
|
|
|
117
171
|
setTimeoutId(newTimeoutId as any);
|
|
118
172
|
} else {
|
|
119
173
|
setIsRetrying(false);
|
|
174
|
+
isPollingRef.current = false; // Release lock
|
|
120
175
|
logger.error('Max retries reached after errors');
|
|
121
176
|
}
|
|
122
177
|
}
|
|
123
178
|
};
|
|
124
179
|
|
|
125
180
|
useEffect(() => {
|
|
126
|
-
if (state.session.session_id) {
|
|
181
|
+
if (state.session.session_id && !isSuccess && !isFailed && !isPollingRef.current) {
|
|
182
|
+
isPollingRef.current = true; // Lock the gate instantly
|
|
127
183
|
getVerificationResult();
|
|
128
184
|
}
|
|
129
|
-
}, [state.session.session_id]);
|
|
185
|
+
}, [state.session.session_id, isSuccess, isFailed]);
|
|
130
186
|
|
|
131
187
|
useEffect(() => {
|
|
132
188
|
return () => {
|
|
133
189
|
if (timeoutId) clearTimeout(timeoutId);
|
|
190
|
+
isPollingRef.current = false; // Reset lock on unmount
|
|
134
191
|
};
|
|
135
192
|
}, [timeoutId]);
|
|
136
193
|
|
|
137
|
-
// 🚨 THE FIX: Automatically trigger the webhook/completion flow on success
|
|
138
194
|
useEffect(() => {
|
|
139
195
|
let autoSubmitTimer: ReturnType<typeof setTimeout>;
|
|
140
|
-
|
|
141
196
|
if (isSuccess) {
|
|
142
197
|
logger.log("Verification successful. Initiating auto-submit countdown...");
|
|
143
|
-
// Wait 3 seconds so the user can see the "Complete" screen, then automatically push the data
|
|
144
198
|
autoSubmitTimer = setTimeout(() => {
|
|
145
199
|
actions.submitVerification();
|
|
146
200
|
}, 3000);
|
|
147
201
|
}
|
|
148
|
-
|
|
149
|
-
// Cleanup the timer if the user clicks the button manually before 3 seconds are up
|
|
150
202
|
return () => {
|
|
151
203
|
if (autoSubmitTimer) clearTimeout(autoSubmitTimer);
|
|
152
204
|
};
|
|
@@ -171,26 +223,22 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
171
223
|
return (
|
|
172
224
|
<View style={styles.container}>
|
|
173
225
|
<Text style={styles.title}>{t('kyc.verificationProgress.status.failed')}</Text>
|
|
174
|
-
|
|
175
226
|
<View style={styles.issuesBox}>
|
|
176
227
|
<Text style={styles.issuesTitle}>{t('common.error')}</Text>
|
|
177
228
|
{Array.isArray(verification.result?.issues) && verification.result.issues.map((issue: string, idx: number) => (
|
|
178
229
|
<Text key={idx} style={styles.issueItem}>• {issue}</Text>
|
|
179
230
|
))}
|
|
180
231
|
</View>
|
|
181
|
-
|
|
182
232
|
<TouchableOpacity style={[styles.button, styles.primary]} onPress={() => actions.submitVerification()}>
|
|
183
233
|
<Text style={styles.buttonText}>
|
|
184
234
|
{state.currentLanguage === 'en' ? 'OK / Finish' : 'Terminer'}
|
|
185
235
|
</Text>
|
|
186
236
|
</TouchableOpacity>
|
|
187
|
-
|
|
188
237
|
<TouchableOpacity style={[styles.button, styles.secondary]} onPress={() => actions.resetTemplate()}>
|
|
189
238
|
<Text style={styles.secondaryText}>
|
|
190
239
|
{state.currentLanguage === 'en' ? 'Start Over' : 'Recommencer'}
|
|
191
240
|
</Text>
|
|
192
241
|
</TouchableOpacity>
|
|
193
|
-
|
|
194
242
|
{verification.result?.referenceId ? (
|
|
195
243
|
<Text style={styles.refText}>{`Reference ID: ${verification.result.referenceId}`}</Text>
|
|
196
244
|
) : null}
|
|
@@ -202,11 +250,9 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
202
250
|
<View style={styles.container}>
|
|
203
251
|
<Text style={styles.title}>{t('kyc.verificationProgress.title')}</Text>
|
|
204
252
|
<Text style={styles.subtitle}>
|
|
205
|
-
{isRetrying
|
|
206
|
-
? `${t('kyc.verificationProgress.retrying', { current: retryCount, max: 40 })}`
|
|
207
|
-
: t('kyc.verificationProgress.estimatedTime')
|
|
208
|
-
}
|
|
253
|
+
{isRetrying ? `${t('kyc.verificationProgress.retrying', { current: retryCount, max: 40 })}` : t('kyc.verificationProgress.estimatedTime')}
|
|
209
254
|
</Text>
|
|
255
|
+
|
|
210
256
|
<View style={styles.timeline}>
|
|
211
257
|
<View style={[styles.dot, styles.done]}><Text style={styles.stepCheckmark}>✓</Text></View>
|
|
212
258
|
<View style={styles.line} />
|
|
@@ -214,22 +260,23 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
214
260
|
<View style={styles.line} />
|
|
215
261
|
<View style={[styles.dot, styles.pending]}><Text style={styles.stepCheckmark}>3</Text></View>
|
|
216
262
|
</View>
|
|
263
|
+
|
|
217
264
|
<View style={styles.row}>
|
|
218
265
|
<Text style={styles.stepLabel}>{t('kyc.verificationProgress.status.completed')}</Text>
|
|
219
266
|
<Text style={styles.stepLabel}>{t('kyc.verificationProgress.steps.analyzing')}</Text>
|
|
220
267
|
<Text style={styles.stepLabel}>{t('kyc.verificationProgress.steps.verifying')}</Text>
|
|
221
268
|
</View>
|
|
269
|
+
|
|
222
270
|
<View style={styles.card}>
|
|
223
271
|
<Text style={styles.cardTitle}>{t('kyc.idCardCapture.title')}</Text>
|
|
224
272
|
<Text style={styles.cardLine}>{t('kyc.verificationProgress.steps.extracting')}</Text>
|
|
273
|
+
|
|
225
274
|
<Text style={styles.cardTitle}>{t('kyc.selfieCapture.title')}</Text>
|
|
226
275
|
<Text style={styles.cardLine}>{t('kyc.verificationProgress.steps.analyzing')}</Text>
|
|
276
|
+
|
|
227
277
|
<Text style={styles.cardTitle}>{t('kyc.verificationProgress.title')}</Text>
|
|
228
278
|
<Text style={styles.cardLine}>
|
|
229
|
-
{isRetrying
|
|
230
|
-
? `${t('kyc.verificationProgress.status.processing')}...`
|
|
231
|
-
: t('kyc.verificationProgress.status.pending')
|
|
232
|
-
}
|
|
279
|
+
{isRetrying ? `${t('kyc.verificationProgress.status.processing')}...` : t('kyc.verificationProgress.status.pending')}
|
|
233
280
|
</Text>
|
|
234
281
|
</View>
|
|
235
282
|
</View>
|
|
@@ -55,7 +55,7 @@ export interface EnhancedCameraViewProps {
|
|
|
55
55
|
cameraType?: 'front' | 'back';
|
|
56
56
|
style?: ViewStyle;
|
|
57
57
|
onCapture?: (result: { success: boolean; path?: string; error?: string }) => void;
|
|
58
|
-
onSilentCapture?: (result: { success: boolean; path?: string; error?: string }) => void;
|
|
58
|
+
onSilentCapture?: (result: { success: boolean; path?: string; error?: string; barcode:unknown }) => void;
|
|
59
59
|
silentCaptureResult?: ISilentCaptureResult;
|
|
60
60
|
onError?: (event: { message: string }) => void;
|
|
61
61
|
onClose?: () => void;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
MultiFormatReader,
|
|
4
|
+
BarcodeFormat,
|
|
5
|
+
DecodeHintType,
|
|
6
|
+
HTMLCanvasElementLuminanceSource,
|
|
7
|
+
BinaryBitmap,
|
|
8
|
+
HybridBinarizer,
|
|
9
|
+
GlobalHistogramBinarizer,
|
|
10
|
+
} from '@zxing/library';
|
|
11
|
+
|
|
12
|
+
interface WebBarcodeScannerProps {
|
|
13
|
+
videoRef: React.RefObject<HTMLVideoElement | null>;
|
|
14
|
+
active: boolean;
|
|
15
|
+
onBarcodeScanned: (value: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const WebBarcodeScanner: React.FC<WebBarcodeScannerProps> = ({
|
|
19
|
+
videoRef,
|
|
20
|
+
active,
|
|
21
|
+
onBarcodeScanned,
|
|
22
|
+
}) => {
|
|
23
|
+
const readerRef = useRef<MultiFormatReader | null>(null);
|
|
24
|
+
|
|
25
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
26
|
+
|
|
27
|
+
const lastValueRef = useRef<string | null>(null);
|
|
28
|
+
|
|
29
|
+
const stoppedRef = useRef(false);
|
|
30
|
+
|
|
31
|
+
const callbackRef = useRef(onBarcodeScanned);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
callbackRef.current = onBarcodeScanned;
|
|
35
|
+
}, [onBarcodeScanned]);
|
|
36
|
+
|
|
37
|
+
const decodeBitmap = useCallback(
|
|
38
|
+
(canvas: HTMLCanvasElement) => {
|
|
39
|
+
if (!readerRef.current) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const source = new HTMLCanvasElementLuminanceSource(canvas);
|
|
44
|
+
|
|
45
|
+
const attempts = [
|
|
46
|
+
new BinaryBitmap(
|
|
47
|
+
new HybridBinarizer(source)
|
|
48
|
+
),
|
|
49
|
+
new BinaryBitmap(
|
|
50
|
+
new GlobalHistogramBinarizer(source)
|
|
51
|
+
),
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
for (const bitmap of attempts) {
|
|
55
|
+
try {
|
|
56
|
+
const result =
|
|
57
|
+
readerRef.current.decodeWithState(bitmap);
|
|
58
|
+
|
|
59
|
+
if (result?.getText()) {
|
|
60
|
+
return result.getText();
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// continue
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
},
|
|
69
|
+
[]
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const drawROI = useCallback(
|
|
73
|
+
(
|
|
74
|
+
video: HTMLVideoElement,
|
|
75
|
+
canvas: HTMLCanvasElement
|
|
76
|
+
) => {
|
|
77
|
+
const ctx = canvas.getContext('2d', {
|
|
78
|
+
willReadFrequently: true,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!ctx) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const roi = 0.8;
|
|
86
|
+
|
|
87
|
+
const sw = Math.floor(
|
|
88
|
+
video.videoWidth * roi
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const sh = Math.floor(
|
|
92
|
+
video.videoHeight * roi
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const sx = Math.floor(
|
|
96
|
+
(video.videoWidth - sw) / 2
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const sy = Math.floor(
|
|
100
|
+
(video.videoHeight - sh) / 2
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
canvas.width !== sw ||
|
|
105
|
+
canvas.height !== sh
|
|
106
|
+
) {
|
|
107
|
+
canvas.width = sw;
|
|
108
|
+
canvas.height = sh;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
ctx.drawImage(
|
|
112
|
+
video,
|
|
113
|
+
sx,
|
|
114
|
+
sy,
|
|
115
|
+
sw,
|
|
116
|
+
sh,
|
|
117
|
+
0,
|
|
118
|
+
0,
|
|
119
|
+
sw,
|
|
120
|
+
sh
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
},
|
|
125
|
+
[]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Add a persistent rotation canvas ref at the top
|
|
129
|
+
const rotationCanvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
130
|
+
|
|
131
|
+
// Update rotateCanvas to re-use the persistent canvas
|
|
132
|
+
const rotateCanvas = useCallback(
|
|
133
|
+
(sourceCanvas: HTMLCanvasElement, degrees: number) => {
|
|
134
|
+
if (!rotationCanvasRef.current) {
|
|
135
|
+
rotationCanvasRef.current = document.createElement('canvas');
|
|
136
|
+
}
|
|
137
|
+
const rotated = rotationCanvasRef.current;
|
|
138
|
+
const ctx = rotated.getContext('2d');
|
|
139
|
+
if (!ctx) return null;
|
|
140
|
+
|
|
141
|
+
const radians = (degrees * Math.PI) / 180;
|
|
142
|
+
if (degrees === 90 || degrees === -90) {
|
|
143
|
+
rotated.width = sourceCanvas.height;
|
|
144
|
+
rotated.height = sourceCanvas.width;
|
|
145
|
+
} else {
|
|
146
|
+
rotated.width = sourceCanvas.width;
|
|
147
|
+
rotated.height = sourceCanvas.height;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
ctx.clearRect(0, 0, rotated.width, rotated.height);
|
|
151
|
+
ctx.translate(rotated.width / 2, rotated.height / 2);
|
|
152
|
+
ctx.rotate(radians);
|
|
153
|
+
ctx.drawImage(sourceCanvas, -sourceCanvas.width / 2, -sourceCanvas.height / 2);
|
|
154
|
+
return rotated;
|
|
155
|
+
},
|
|
156
|
+
[]
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
const tryDecode = useCallback(() => {
|
|
162
|
+
const video = videoRef.current;
|
|
163
|
+
|
|
164
|
+
const canvas = canvasRef.current;
|
|
165
|
+
|
|
166
|
+
if (
|
|
167
|
+
!video ||
|
|
168
|
+
!canvas ||
|
|
169
|
+
video.readyState < 2
|
|
170
|
+
) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const ok = drawROI(video, canvas);
|
|
175
|
+
|
|
176
|
+
if (!ok) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let value = decodeBitmap(canvas);
|
|
181
|
+
|
|
182
|
+
if (value) {
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const rotations = [90, -90, 180];
|
|
187
|
+
|
|
188
|
+
for (const rotation of rotations) {
|
|
189
|
+
const rotated = rotateCanvas(
|
|
190
|
+
canvas,
|
|
191
|
+
rotation
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (!rotated) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
value = decodeBitmap(rotated);
|
|
199
|
+
|
|
200
|
+
if (value) {
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return null;
|
|
206
|
+
}, [
|
|
207
|
+
videoRef,
|
|
208
|
+
drawROI,
|
|
209
|
+
decodeBitmap,
|
|
210
|
+
rotateCanvas,
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
const tick = useCallback(() => {
|
|
214
|
+
if (stoppedRef.current) return;
|
|
215
|
+
|
|
216
|
+
const value = tryDecode();
|
|
217
|
+
if (value && value !== lastValueRef.current) {
|
|
218
|
+
lastValueRef.current = value;
|
|
219
|
+
callbackRef.current(value);
|
|
220
|
+
// If you want it to stop scanning on success, leave 'return;'
|
|
221
|
+
// but ensure your parent component handles unmounting the scanner!
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
setTimeout(tick, 100);
|
|
226
|
+
}, [tryDecode]);
|
|
227
|
+
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (!active) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
stoppedRef.current = false;
|
|
234
|
+
|
|
235
|
+
lastValueRef.current = null;
|
|
236
|
+
|
|
237
|
+
if (!canvasRef.current) {
|
|
238
|
+
canvasRef.current =
|
|
239
|
+
document.createElement('canvas');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const hints = new Map();
|
|
243
|
+
|
|
244
|
+
hints.set(
|
|
245
|
+
DecodeHintType.POSSIBLE_FORMATS,
|
|
246
|
+
[BarcodeFormat.PDF_417]
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
hints.set(
|
|
250
|
+
DecodeHintType.TRY_HARDER,
|
|
251
|
+
true
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const reader =
|
|
255
|
+
new MultiFormatReader();
|
|
256
|
+
|
|
257
|
+
reader.setHints(hints);
|
|
258
|
+
|
|
259
|
+
readerRef.current = reader;
|
|
260
|
+
|
|
261
|
+
tick();
|
|
262
|
+
|
|
263
|
+
return () => {
|
|
264
|
+
stoppedRef.current = true;
|
|
265
|
+
|
|
266
|
+
readerRef.current?.reset();
|
|
267
|
+
|
|
268
|
+
readerRef.current = null;
|
|
269
|
+
};
|
|
270
|
+
}, [active, tick]);
|
|
271
|
+
|
|
272
|
+
return null;
|
|
273
|
+
};
|
package/src/config/KYCConfigs.ts
CHANGED
|
@@ -56,7 +56,7 @@ export const basicKYCConfig: KYCFlowConfig = {
|
|
|
56
56
|
validation: { required: true },
|
|
57
57
|
cameraConfig: {
|
|
58
58
|
aspectRatio: 4/3,
|
|
59
|
-
quality:
|
|
59
|
+
quality: 1,
|
|
60
60
|
flashMode: 'auto',
|
|
61
61
|
cameraType: 'back',
|
|
62
62
|
allowRetake: true,
|
|
@@ -78,7 +78,7 @@ export const basicKYCConfig: KYCFlowConfig = {
|
|
|
78
78
|
validation: { required: true },
|
|
79
79
|
cameraConfig: {
|
|
80
80
|
aspectRatio: 4/3,
|
|
81
|
-
quality:
|
|
81
|
+
quality: 1,
|
|
82
82
|
flashMode: 'auto',
|
|
83
83
|
cameraType: 'back',
|
|
84
84
|
allowRetake: true,
|
|
@@ -275,20 +275,6 @@
|
|
|
275
275
|
"py_file": "ontario_front.py",
|
|
276
276
|
"keys_found": null
|
|
277
277
|
}
|
|
278
|
-
],
|
|
279
|
-
"Quebec": [
|
|
280
|
-
{
|
|
281
|
-
"py_file": "quebec_01_back.py",
|
|
282
|
-
"back": "2D_barcode"
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
"py_file": "quebec_01_front.py",
|
|
286
|
-
"keys_found": null
|
|
287
|
-
},
|
|
288
|
-
{
|
|
289
|
-
"py_file": "quebec_02_front.py",
|
|
290
|
-
"front": "2D_barcode"
|
|
291
|
-
}
|
|
292
278
|
]
|
|
293
279
|
},
|
|
294
280
|
"identity_card": {
|