@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.
@@ -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
- const [timeoutId, setTimeoutId] = useState<number | null>(null);
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, maxRetries = 60) => {
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}/${maxRetries + 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; // Release lock
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; // Fallback to root if .data is missing
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
- // const processingError = vData.reason || vData.error || vData.issues || vData.error_message;
76
- const selfieMatch = vData.selfie_info?.is_match; // true, false, or null
77
-
79
+ const selfieMatch = vData.selfie_info?.is_match;
80
+
78
81
  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.');
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: '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
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
- // 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
-
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 < maxRetries) {
117
+ if (currentRetryCount < MAX_RETRIES) {
124
118
  const nextRetryCount = currentRetryCount + 1;
125
- setRetryCount(nextRetryCount);
126
- setIsRetrying(true);
127
-
128
- logger.log(`Verification still processing, retrying in 3s... (${nextRetryCount}/${maxRetries})`);
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
- setTimeoutId(newTimeoutId as any);
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
- // Fallback release if status is completely unknown
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 < maxRetries) {
152
+ if (currentRetryCount < MAX_RETRIES) {
160
153
  const nextRetryCount = currentRetryCount + 1;
161
- setRetryCount(nextRetryCount);
162
- setIsRetrying(true);
163
-
164
- logger.log(`getVerificationResult Error occurred, retrying in 3s... (${nextRetryCount}/${maxRetries})`);
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 (timeoutId) clearTimeout(timeoutId);
167
- const newTimeoutId = setTimeout(() => {
168
- getVerificationResult(nextRetryCount, maxRetries);
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; // Lock the gate instantly
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
- if (timeoutId) clearTimeout(timeoutId);
190
- isPollingRef.current = false; // Reset lock on unmount
191
+ isMountedRef.current = false;
192
+ if (timeoutIdRef.current) clearTimeout(timeoutIdRef.current);
193
+ isPollingRef.current = false;
191
194
  };
192
- }, [timeoutId]);
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 ? `${t('kyc.verificationProgress.retrying', { current: retryCount, max: 40 })}` : t('kyc.verificationProgress.estimatedTime')}
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')}