@sanctum-key/react-native-sdk 1.0.43 → 1.0.45
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 +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +49 -1
- 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 +81 -56
- package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/IDCardCapture.tsx +61 -1
- package/src/components/KYCElements/VerificationProgressTemplate.tsx +107 -93
|
@@ -14,25 +14,33 @@ interface VerificationProgressTemplateProps {
|
|
|
14
14
|
language?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const MAX_RETRIES = 40; // 40 attempts * 3 seconds = exactly 2 minutes of polling
|
|
18
|
+
|
|
17
19
|
export const VerificationProgressTemplate: React.FC<VerificationProgressTemplateProps> = () => {
|
|
18
20
|
const { t } = useI18n();
|
|
19
21
|
const { state, actions } = useTemplateKYCFlowContext();
|
|
22
|
+
|
|
20
23
|
const [retryCount, setRetryCount] = useState(0);
|
|
21
24
|
const [isRetrying, setIsRetrying] = useState(false);
|
|
22
|
-
|
|
25
|
+
|
|
26
|
+
// 🚨 BULLETPROOF REFS: Prevents memory leaks and duplicate polling loops
|
|
27
|
+
const timeoutIdRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
23
28
|
const isPollingRef = useRef(false);
|
|
29
|
+
const isMountedRef = useRef(true);
|
|
24
30
|
|
|
25
31
|
const verification = state.verification;
|
|
26
32
|
const isSuccess = verification.status === 'success';
|
|
27
33
|
const isFailed = verification.status === 'failed';
|
|
28
34
|
|
|
29
|
-
const getVerificationResult = async (currentRetryCount = 0
|
|
35
|
+
const getVerificationResult = async (currentRetryCount = 0) => {
|
|
36
|
+
// If component unmounted while waiting, stop everything
|
|
37
|
+
if (!isMountedRef.current) return;
|
|
38
|
+
|
|
30
39
|
try {
|
|
31
|
-
logger.log(`Starting verification check (attempt ${currentRetryCount + 1}/${
|
|
40
|
+
logger.log(`Starting verification check (attempt ${currentRetryCount + 1}/${MAX_RETRIES})`);
|
|
32
41
|
const result: any = await kycService.getVerificationResult(state.session.session_id);
|
|
33
|
-
|
|
34
42
|
logger.log('getVerificationResult raw result:', JSON.stringify(result, null, 2));
|
|
35
|
-
|
|
43
|
+
|
|
36
44
|
const rootStatus = (result?.status || '').toLowerCase();
|
|
37
45
|
if (rootStatus === 'failed.' || rootStatus === 'failed' || rootStatus === 'rejected' || rootStatus === 'error') {
|
|
38
46
|
actions.setVerificationState({
|
|
@@ -42,22 +50,18 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
42
50
|
issues: [result?.message || result?.reason || 'Verification failed. Sent for manual review.']
|
|
43
51
|
} as any
|
|
44
52
|
});
|
|
45
|
-
setIsRetrying(false);
|
|
46
|
-
isPollingRef.current = false;
|
|
53
|
+
if (isMountedRef.current) setIsRetrying(false);
|
|
54
|
+
isPollingRef.current = false;
|
|
47
55
|
return;
|
|
48
56
|
}
|
|
49
|
-
|
|
57
|
+
|
|
50
58
|
// 2. Safely Extract Nested Payload
|
|
51
|
-
let vData = result?.data || result;
|
|
52
|
-
|
|
53
|
-
// Handle stringified JSON
|
|
59
|
+
let vData = result?.data || result;
|
|
60
|
+
|
|
54
61
|
if (typeof vData === 'string') {
|
|
55
|
-
try {
|
|
56
|
-
vData = JSON.parse(vData);
|
|
57
|
-
} catch (e) { }
|
|
62
|
+
try { vData = JSON.parse(vData); } catch (e) { }
|
|
58
63
|
}
|
|
59
|
-
|
|
60
|
-
// Handle backend dictionaries indexed by session_id
|
|
64
|
+
|
|
61
65
|
if (vData && typeof vData === 'object' && vData[state.session.session_id]) {
|
|
62
66
|
let nested = vData[state.session.session_id];
|
|
63
67
|
if (nested && nested.data) {
|
|
@@ -65,132 +69,132 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
65
69
|
}
|
|
66
70
|
vData = nested;
|
|
67
71
|
}
|
|
68
|
-
|
|
72
|
+
|
|
69
73
|
if (!vData || typeof vData !== 'object') {
|
|
70
74
|
throw new Error('Verification payload missing or invalid for current session');
|
|
71
75
|
}
|
|
72
|
-
|
|
76
|
+
|
|
73
77
|
const vStatus = (vData.verification_status || vData.status || vData.state || '').toLowerCase();
|
|
74
78
|
const vDecision = (vData.decision || vData.outcome || vData.result || '').toLowerCase();
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
const selfieMatch = vData.selfie_info?.is_match;
|
|
80
|
+
|
|
78
81
|
logger.log(`Parsed Status: [${vStatus}], Decision: [${vDecision}], Selfie Match: [${selfieMatch}]`);
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
82
|
+
|
|
83
|
+
// SUCCESS CONDITION
|
|
84
|
+
if (
|
|
85
|
+
vStatus === 'approved' || vStatus === 'verified' || vStatus === 'success' ||
|
|
86
|
+
vDecision === 'approved' || vDecision === 'passed' || vDecision === 'clear' ||
|
|
87
|
+
(vStatus === 'completed' && selfieMatch === true)
|
|
88
|
+
) {
|
|
89
|
+
const hasActualData = vData && Object.keys(vData).length > 0;
|
|
90
|
+
if (!hasActualData) {
|
|
91
|
+
actions.setVerificationState({
|
|
92
|
+
status: 'failed',
|
|
93
|
+
result: {
|
|
94
|
+
referenceId: state.session.session_id,
|
|
95
|
+
issues: ['Document text could not be read clearly. Please retake the photo in better lighting.']
|
|
96
|
+
} as any
|
|
97
|
+
});
|
|
98
|
+
if (isMountedRef.current) setIsRetrying(false);
|
|
99
|
+
isPollingRef.current = false;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
94
103
|
actions.setVerificationState({
|
|
95
|
-
status: '
|
|
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
|
|
104
|
+
status: 'success',
|
|
105
|
+
result: { ...result, referenceId: vData.verification_id || vData.session_id || state.session.session_id } as any
|
|
100
106
|
});
|
|
101
|
-
setIsRetrying(false);
|
|
107
|
+
if (isMountedRef.current) setIsRetrying(false);
|
|
102
108
|
isPollingRef.current = false;
|
|
103
109
|
return;
|
|
104
110
|
}
|
|
105
111
|
|
|
106
|
-
//
|
|
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
|
-
|
|
112
|
+
// PENDING/PROCESSING CONDITION
|
|
119
113
|
if (
|
|
120
114
|
vStatus === 'processing' || vStatus === 'pending' || vStatus === 'in_progress' ||
|
|
121
115
|
vStatus === 'initiated' || (vStatus === 'completed' && selfieMatch === null) || !vStatus
|
|
122
116
|
) {
|
|
123
|
-
if (currentRetryCount <
|
|
117
|
+
if (currentRetryCount < MAX_RETRIES) {
|
|
124
118
|
const nextRetryCount = currentRetryCount + 1;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
131
|
-
const newTimeoutId = setTimeout(() => {
|
|
132
|
-
getVerificationResult(nextRetryCount, maxRetries);
|
|
133
|
-
}, 3000);
|
|
119
|
+
if (isMountedRef.current) {
|
|
120
|
+
setRetryCount(nextRetryCount);
|
|
121
|
+
setIsRetrying(true);
|
|
122
|
+
}
|
|
123
|
+
logger.log(`Verification still processing, retrying in 3s... (${nextRetryCount}/${MAX_RETRIES})`);
|
|
134
124
|
|
|
135
|
-
|
|
125
|
+
if (timeoutIdRef.current) clearTimeout(timeoutIdRef.current);
|
|
126
|
+
timeoutIdRef.current = setTimeout(() => {
|
|
127
|
+
getVerificationResult(nextRetryCount);
|
|
128
|
+
}, 3000);
|
|
136
129
|
} else {
|
|
137
130
|
logger.error('Max retries reached, verification still processing');
|
|
138
|
-
setIsRetrying(false);
|
|
139
|
-
isPollingRef.current = false;
|
|
140
131
|
actions.setVerificationState({
|
|
141
132
|
status: 'failed',
|
|
142
133
|
result: {
|
|
143
134
|
referenceId: state.session.session_id,
|
|
144
|
-
issues: ['Verification timed out. Please try again.']
|
|
135
|
+
issues: ['Verification timed out after maximum attempts. Please try again.']
|
|
145
136
|
} as any
|
|
146
137
|
});
|
|
138
|
+
if (isMountedRef.current) setIsRetrying(false);
|
|
139
|
+
isPollingRef.current = false;
|
|
147
140
|
}
|
|
148
141
|
return;
|
|
149
142
|
}
|
|
150
|
-
|
|
151
|
-
//
|
|
143
|
+
|
|
144
|
+
// UNKNOWN FALLBACK
|
|
152
145
|
logger.warn(`Unknown verification status received: ${vStatus}`);
|
|
153
|
-
setIsRetrying(false);
|
|
146
|
+
if (isMountedRef.current) setIsRetrying(false);
|
|
154
147
|
isPollingRef.current = false;
|
|
155
|
-
|
|
148
|
+
|
|
156
149
|
} catch (error: any) {
|
|
157
150
|
logger.error('getVerificationResult error', JSON.stringify(errorMessage(error) || { message: error?.message || 'Unknown error' }, null, 2));
|
|
158
151
|
|
|
159
|
-
if (currentRetryCount <
|
|
152
|
+
if (currentRetryCount < MAX_RETRIES) {
|
|
160
153
|
const nextRetryCount = currentRetryCount + 1;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
154
|
+
if (isMountedRef.current) {
|
|
155
|
+
setRetryCount(nextRetryCount);
|
|
156
|
+
setIsRetrying(true);
|
|
157
|
+
}
|
|
158
|
+
logger.log(`Error occurred, retrying in 3s... (${nextRetryCount}/${MAX_RETRIES})`);
|
|
165
159
|
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
getVerificationResult(nextRetryCount
|
|
160
|
+
if (timeoutIdRef.current) clearTimeout(timeoutIdRef.current);
|
|
161
|
+
timeoutIdRef.current = setTimeout(() => {
|
|
162
|
+
getVerificationResult(nextRetryCount);
|
|
169
163
|
}, 3000);
|
|
170
|
-
|
|
171
|
-
setTimeoutId(newTimeoutId as any);
|
|
172
164
|
} else {
|
|
173
|
-
setIsRetrying(false);
|
|
174
|
-
isPollingRef.current = false; // Release lock
|
|
175
165
|
logger.error('Max retries reached after errors');
|
|
166
|
+
actions.setVerificationState({
|
|
167
|
+
status: 'failed',
|
|
168
|
+
result: {
|
|
169
|
+
referenceId: state.session.session_id,
|
|
170
|
+
issues: ['Connection failed multiple times. Please check your network and try again.']
|
|
171
|
+
} as any
|
|
172
|
+
});
|
|
173
|
+
if (isMountedRef.current) setIsRetrying(false);
|
|
174
|
+
isPollingRef.current = false;
|
|
176
175
|
}
|
|
177
176
|
}
|
|
178
177
|
};
|
|
179
178
|
|
|
179
|
+
// 1. Kick off polling when component mounts
|
|
180
180
|
useEffect(() => {
|
|
181
|
+
isMountedRef.current = true;
|
|
181
182
|
if (state.session.session_id && !isSuccess && !isFailed && !isPollingRef.current) {
|
|
182
|
-
isPollingRef.current = true;
|
|
183
|
-
getVerificationResult();
|
|
183
|
+
isPollingRef.current = true;
|
|
184
|
+
getVerificationResult(0); // Always start explicitly at 0
|
|
184
185
|
}
|
|
185
186
|
}, [state.session.session_id, isSuccess, isFailed]);
|
|
186
187
|
|
|
188
|
+
// 2. Strict cleanup on unmount ONLY
|
|
187
189
|
useEffect(() => {
|
|
188
190
|
return () => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
isMountedRef.current = false;
|
|
192
|
+
if (timeoutIdRef.current) clearTimeout(timeoutIdRef.current);
|
|
193
|
+
isPollingRef.current = false;
|
|
191
194
|
};
|
|
192
|
-
}, [
|
|
195
|
+
}, []);
|
|
193
196
|
|
|
197
|
+
// 3. Auto-submit on success
|
|
194
198
|
useEffect(() => {
|
|
195
199
|
let autoSubmitTimer: ReturnType<typeof setTimeout>;
|
|
196
200
|
if (isSuccess) {
|
|
@@ -204,14 +208,18 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
204
208
|
};
|
|
205
209
|
}, [isSuccess, actions]);
|
|
206
210
|
|
|
211
|
+
// --- RENDER LOGIC ---
|
|
212
|
+
|
|
207
213
|
if (isSuccess) {
|
|
208
214
|
return (
|
|
209
215
|
<View style={styles.container}>
|
|
210
216
|
<Text style={styles.title}>{t('kyc.verificationProgress.steps.complete')}</Text>
|
|
211
217
|
<Text style={styles.subtitle}>{t('kyc.verificationProgress.subtitle')}</Text>
|
|
218
|
+
|
|
212
219
|
<TouchableOpacity style={[styles.button, styles.primary]} onPress={() => actions.submitVerification()}>
|
|
213
220
|
<Text style={styles.buttonText}>{t('common.continue')}</Text>
|
|
214
221
|
</TouchableOpacity>
|
|
222
|
+
|
|
215
223
|
{verification.result?.referenceId ? (
|
|
216
224
|
<Text style={styles.refText}>{`Verification ID: ${verification.result.referenceId}`}</Text>
|
|
217
225
|
) : null}
|
|
@@ -223,22 +231,26 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
223
231
|
return (
|
|
224
232
|
<View style={styles.container}>
|
|
225
233
|
<Text style={styles.title}>{t('kyc.verificationProgress.status.failed')}</Text>
|
|
234
|
+
|
|
226
235
|
<View style={styles.issuesBox}>
|
|
227
236
|
<Text style={styles.issuesTitle}>{t('common.error')}</Text>
|
|
228
237
|
{Array.isArray(verification.result?.issues) && verification.result.issues.map((issue: string, idx: number) => (
|
|
229
238
|
<Text key={idx} style={styles.issueItem}>• {issue}</Text>
|
|
230
239
|
))}
|
|
231
240
|
</View>
|
|
241
|
+
|
|
232
242
|
<TouchableOpacity style={[styles.button, styles.primary]} onPress={() => actions.submitVerification()}>
|
|
233
243
|
<Text style={styles.buttonText}>
|
|
234
244
|
{state.currentLanguage === 'en' ? 'OK / Finish' : 'Terminer'}
|
|
235
245
|
</Text>
|
|
236
246
|
</TouchableOpacity>
|
|
247
|
+
|
|
237
248
|
<TouchableOpacity style={[styles.button, styles.secondary]} onPress={() => actions.resetTemplate()}>
|
|
238
249
|
<Text style={styles.secondaryText}>
|
|
239
250
|
{state.currentLanguage === 'en' ? 'Start Over' : 'Recommencer'}
|
|
240
251
|
</Text>
|
|
241
252
|
</TouchableOpacity>
|
|
253
|
+
|
|
242
254
|
{verification.result?.referenceId ? (
|
|
243
255
|
<Text style={styles.refText}>{`Reference ID: ${verification.result.referenceId}`}</Text>
|
|
244
256
|
) : null}
|
|
@@ -250,7 +262,9 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
250
262
|
<View style={styles.container}>
|
|
251
263
|
<Text style={styles.title}>{t('kyc.verificationProgress.title')}</Text>
|
|
252
264
|
<Text style={styles.subtitle}>
|
|
253
|
-
{isRetrying
|
|
265
|
+
{isRetrying
|
|
266
|
+
? `${t('kyc.verificationProgress.retrying', { current: retryCount, max: MAX_RETRIES })}`
|
|
267
|
+
: t('kyc.verificationProgress.estimatedTime')}
|
|
254
268
|
</Text>
|
|
255
269
|
|
|
256
270
|
<View style={styles.timeline}>
|
|
@@ -270,10 +284,10 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
270
284
|
<View style={styles.card}>
|
|
271
285
|
<Text style={styles.cardTitle}>{t('kyc.idCardCapture.title')}</Text>
|
|
272
286
|
<Text style={styles.cardLine}>{t('kyc.verificationProgress.steps.extracting')}</Text>
|
|
273
|
-
|
|
287
|
+
|
|
274
288
|
<Text style={styles.cardTitle}>{t('kyc.selfieCapture.title')}</Text>
|
|
275
289
|
<Text style={styles.cardLine}>{t('kyc.verificationProgress.steps.analyzing')}</Text>
|
|
276
|
-
|
|
290
|
+
|
|
277
291
|
<Text style={styles.cardTitle}>{t('kyc.verificationProgress.title')}</Text>
|
|
278
292
|
<Text style={styles.cardLine}>
|
|
279
293
|
{isRetrying ? `${t('kyc.verificationProgress.status.processing')}...` : t('kyc.verificationProgress.status.pending')}
|