@sanctum-key/react-native-sdk 1.0.11 → 1.0.13
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/EmailVerificationTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/EmailVerificationTemplate.js +69 -37
- package/build/src/components/KYCElements/EmailVerificationTemplate.js.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +129 -181
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/components/KYCElements/PhoneVerificationTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/PhoneVerificationTemplate.js +97 -65
- package/build/src/components/KYCElements/PhoneVerificationTemplate.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +1 -1
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/EmailVerificationTemplate.tsx +127 -95
- package/src/components/KYCElements/IDCardCapture.tsx +226 -296
- package/src/components/KYCElements/PhoneVerificationTemplate.tsx +185 -165
- package/src/modules/api/KYCService.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
-
import { View, Text, StyleSheet, TextInput, TouchableOpacity, Alert, Pressable } from 'react-native';
|
|
2
|
+
import { View, Text, StyleSheet, TextInput, TouchableOpacity, Alert, Pressable, Platform } from 'react-native';
|
|
3
3
|
import { TemplateComponent, LocalizedText } from '../../types/KYC.types';
|
|
4
4
|
import { useTemplateKYCFlowContext } from '../../hooks/useTemplateKYCFlow';
|
|
5
5
|
import { useI18n } from '../../hooks/useI18n';
|
|
@@ -38,7 +38,9 @@ export const EmailVerificationTemplate: React.FC<EmailVerificationTemplateProps>
|
|
|
38
38
|
const [otp, setOtp] = useState('');
|
|
39
39
|
const [localError, setLocalError] = useState<string | null>(null);
|
|
40
40
|
const [isSimulating, setIsSimulating] = useState(false);
|
|
41
|
-
|
|
41
|
+
|
|
42
|
+
// Track actual focus state for visual feedback
|
|
43
|
+
const [isInputFocused, setIsInputFocused] = useState(false);
|
|
42
44
|
const inputRef = useRef<TextInput>(null);
|
|
43
45
|
|
|
44
46
|
const title = getLocalizedText(component.labels as LocalizedText);
|
|
@@ -49,28 +51,36 @@ export const EmailVerificationTemplate: React.FC<EmailVerificationTemplateProps>
|
|
|
49
51
|
const sendButtonText = t('common.sendCode') || 'Send Verification Code';
|
|
50
52
|
const buttonText = step === 'email' ? sendButtonText : verifyButtonText;
|
|
51
53
|
|
|
52
|
-
// --- AUTO SUBMIT LOGIC ---
|
|
53
54
|
useEffect(() => {
|
|
54
55
|
if (otp.length === CODE_LENGTH && step === 'otp' && !isSimulating) {
|
|
55
56
|
handleVerifyCode();
|
|
56
57
|
}
|
|
57
58
|
}, [otp]);
|
|
58
59
|
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
let focusTimer: ReturnType<typeof setTimeout>;
|
|
62
|
+
if (step === 'otp' && !isSimulating) {
|
|
63
|
+
focusTimer = setTimeout(() => {
|
|
64
|
+
inputRef.current?.focus();
|
|
65
|
+
}, 300);
|
|
66
|
+
}
|
|
67
|
+
return () => {
|
|
68
|
+
if (focusTimer) clearTimeout(focusTimer);
|
|
69
|
+
};
|
|
70
|
+
}, [step, isSimulating]);
|
|
71
|
+
|
|
59
72
|
const handleSendCode = async () => {
|
|
60
73
|
const trimmed = email.trim();
|
|
61
74
|
if (!trimmed || !isValidEmail(trimmed)) {
|
|
62
75
|
setLocalError(t('errors.invalidEmail') || 'Please enter a valid email address');
|
|
63
76
|
return;
|
|
64
77
|
}
|
|
65
|
-
|
|
66
78
|
setLocalError(null);
|
|
67
79
|
setIsSimulating(true);
|
|
68
80
|
|
|
69
81
|
try {
|
|
70
82
|
await kycService.sendEmailVerificationCode(trimmed, auth);
|
|
71
83
|
setStep('otp');
|
|
72
|
-
// Auto-focus the OTP input shortly after switching steps
|
|
73
|
-
setTimeout(() => inputRef.current?.focus(), 100);
|
|
74
84
|
} catch (err: any) {
|
|
75
85
|
const msg = errorMessage(err) ?? err?.message ?? (t('errors.sendCodeFailed') || 'Failed to send verification code');
|
|
76
86
|
setLocalError(typeof msg === 'string' ? msg : JSON.stringify(msg));
|
|
@@ -84,7 +94,6 @@ export const EmailVerificationTemplate: React.FC<EmailVerificationTemplateProps>
|
|
|
84
94
|
setLocalError(t('errors.invalidCode') || 'Please enter the 6-digit code');
|
|
85
95
|
return;
|
|
86
96
|
}
|
|
87
|
-
|
|
88
97
|
setLocalError(null);
|
|
89
98
|
setIsSimulating(true);
|
|
90
99
|
|
|
@@ -127,16 +136,16 @@ export const EmailVerificationTemplate: React.FC<EmailVerificationTemplateProps>
|
|
|
127
136
|
<Pressable style={styles.otpBoxesContainer} onPress={() => inputRef.current?.focus()}>
|
|
128
137
|
{boxes.map((_, index) => {
|
|
129
138
|
const digit = otp[index] || '';
|
|
130
|
-
const isCurrent = index === otp.length;
|
|
131
139
|
const isFilled = index < otp.length;
|
|
132
|
-
|
|
140
|
+
const isActiveIndex = index === otp.length || (index === CODE_LENGTH - 1 && otp.length === CODE_LENGTH);
|
|
141
|
+
const isCurrent = isInputFocused && isActiveIndex;
|
|
133
142
|
return (
|
|
134
|
-
<View
|
|
135
|
-
key={index}
|
|
143
|
+
<View
|
|
144
|
+
key={index}
|
|
136
145
|
style={[
|
|
137
|
-
styles.otpBox,
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
styles.otpBox,
|
|
147
|
+
isFilled && styles.otpBoxFilled,
|
|
148
|
+
isCurrent && styles.otpBoxActive
|
|
140
149
|
]}
|
|
141
150
|
>
|
|
142
151
|
<Text style={styles.otpBoxText}>{digit}</Text>
|
|
@@ -148,98 +157,113 @@ export const EmailVerificationTemplate: React.FC<EmailVerificationTemplateProps>
|
|
|
148
157
|
};
|
|
149
158
|
|
|
150
159
|
return (
|
|
151
|
-
<View style={styles.
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
{
|
|
155
|
-
|
|
160
|
+
<View style={styles.wrapper}>
|
|
161
|
+
<View style={styles.container}>
|
|
162
|
+
<Text style={styles.title}>{title}</Text>
|
|
163
|
+
<Text style={styles.instructions}>
|
|
164
|
+
{step === 'email' ? instructions : (t('kyc.enterCodeSent') || `Please enter the code sent to ${email}`)}
|
|
165
|
+
</Text>
|
|
156
166
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
<TextInput
|
|
162
|
-
style={styles.input}
|
|
163
|
-
placeholder="name@example.com"
|
|
164
|
-
value={email}
|
|
165
|
-
onChangeText={onChangeEmail}
|
|
166
|
-
keyboardType="email-address"
|
|
167
|
-
autoCapitalize="none"
|
|
168
|
-
autoCorrect={false}
|
|
169
|
-
editable={!isSimulating}
|
|
170
|
-
/>
|
|
171
|
-
</View>
|
|
172
|
-
) : (
|
|
173
|
-
<View style={styles.inputContainer}>
|
|
174
|
-
<Text style={styles.label}>{t('common.verificationCode') || 'Verification Code'}</Text>
|
|
175
|
-
|
|
176
|
-
<View style={styles.otpWrapper}>
|
|
177
|
-
{renderOtpBoxes()}
|
|
178
|
-
{/* Hidden TextInput overlaid to handle native keyboard & pasting seamlessly */}
|
|
167
|
+
<View style={styles.contentContainer}>
|
|
168
|
+
{step === 'email' ? (
|
|
169
|
+
<View style={styles.inputContainer}>
|
|
170
|
+
<Text style={styles.label}>{t('common.email') || 'Email'}</Text>
|
|
179
171
|
<TextInput
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
172
|
+
style={[
|
|
173
|
+
styles.input,
|
|
174
|
+
Platform.OS === 'web' && { outlineStyle: 'none' } as any
|
|
175
|
+
]}
|
|
176
|
+
placeholder="name@example.com"
|
|
177
|
+
placeholderTextColor="#9CA3AF"
|
|
178
|
+
value={email}
|
|
179
|
+
onChangeText={onChangeEmail}
|
|
180
|
+
keyboardType="email-address"
|
|
181
|
+
autoCapitalize="none"
|
|
182
|
+
autoCorrect={false}
|
|
186
183
|
editable={!isSimulating}
|
|
187
|
-
textContentType="oneTimeCode"
|
|
188
|
-
caretHidden={true}
|
|
189
184
|
/>
|
|
190
185
|
</View>
|
|
186
|
+
) : (
|
|
187
|
+
<View style={styles.inputContainer}>
|
|
188
|
+
<Text style={styles.label}>{t('common.verificationCode') || 'Verification Code'}</Text>
|
|
189
|
+
<View style={styles.otpWrapper}>
|
|
190
|
+
{renderOtpBoxes()}
|
|
191
|
+
<TextInput
|
|
192
|
+
ref={inputRef}
|
|
193
|
+
style={[
|
|
194
|
+
styles.hiddenInput,
|
|
195
|
+
Platform.OS === 'web' && { outlineStyle: 'none' } as any
|
|
196
|
+
]}
|
|
197
|
+
value={otp}
|
|
198
|
+
onChangeText={onChangeOtp}
|
|
199
|
+
keyboardType="number-pad"
|
|
200
|
+
maxLength={CODE_LENGTH}
|
|
201
|
+
editable={!isSimulating}
|
|
202
|
+
textContentType="oneTimeCode"
|
|
203
|
+
caretHidden={true}
|
|
204
|
+
onFocus={() => setIsInputFocused(true)}
|
|
205
|
+
onBlur={() => setIsInputFocused(false)}
|
|
206
|
+
/>
|
|
207
|
+
</View>
|
|
208
|
+
<TouchableOpacity onPress={handleBackToEmail} style={styles.changeEmailLink} disabled={isSimulating}>
|
|
209
|
+
<Text style={styles.changeEmailText}>{t('common.changeEmail') || 'Change email'}</Text>
|
|
210
|
+
</TouchableOpacity>
|
|
211
|
+
</View>
|
|
212
|
+
)}
|
|
191
213
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
</View>
|
|
196
|
-
)}
|
|
197
|
-
|
|
198
|
-
{(localError || propError) && (
|
|
199
|
-
<Text style={styles.errorText}>{localError || propError}</Text>
|
|
200
|
-
)}
|
|
214
|
+
{(localError || propError) && (
|
|
215
|
+
<Text style={styles.errorText}>{localError || propError}</Text>
|
|
216
|
+
)}
|
|
201
217
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
<Button
|
|
219
|
+
title={isSimulating ? (t('common.processing') || 'Processing...') : buttonText}
|
|
220
|
+
onPress={step === 'email' ? handleSendCode : handleVerifyCode}
|
|
221
|
+
style={styles.button}
|
|
222
|
+
disabled={
|
|
223
|
+
isSimulating ||
|
|
224
|
+
(step === 'email' ? !email : otp.length < CODE_LENGTH)
|
|
225
|
+
}
|
|
226
|
+
/>
|
|
211
227
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
228
|
+
{step === 'otp' && (
|
|
229
|
+
<TouchableOpacity
|
|
230
|
+
onPress={async () => {
|
|
231
|
+
if (isSimulating) return;
|
|
232
|
+
setLocalError(null);
|
|
233
|
+
setIsSimulating(true);
|
|
234
|
+
try {
|
|
235
|
+
await kycService.sendEmailVerificationCode(email.trim(), auth);
|
|
236
|
+
Alert.alert(
|
|
237
|
+
t('common.codeResent') || 'Code Resent',
|
|
238
|
+
t('common.codeResentMessage', { email }) || 'Code resent to ' + email
|
|
239
|
+
);
|
|
240
|
+
inputRef.current?.focus();
|
|
241
|
+
} catch (err: any) {
|
|
242
|
+
const msg = errorMessage(err) ?? err?.message ?? (t('errors.sendCodeFailed') || 'Failed to send code');
|
|
243
|
+
setLocalError(typeof msg === 'string' ? msg : JSON.stringify(msg));
|
|
244
|
+
} finally {
|
|
245
|
+
setIsSimulating(false);
|
|
246
|
+
}
|
|
247
|
+
}}
|
|
248
|
+
style={styles.resendButton}
|
|
249
|
+
disabled={isSimulating}
|
|
250
|
+
>
|
|
251
|
+
<Text style={styles.resendText}>{t('common.resendCode') || 'Resend Code'}</Text>
|
|
252
|
+
</TouchableOpacity>
|
|
253
|
+
)}
|
|
254
|
+
</View>
|
|
237
255
|
</View>
|
|
238
256
|
</View>
|
|
239
257
|
);
|
|
240
258
|
};
|
|
241
259
|
|
|
242
260
|
const styles = StyleSheet.create({
|
|
261
|
+
wrapper: {
|
|
262
|
+
flex: 1,
|
|
263
|
+
width: '100%',
|
|
264
|
+
alignItems: 'center',
|
|
265
|
+
justifyContent: 'center',
|
|
266
|
+
},
|
|
243
267
|
container: {
|
|
244
268
|
padding: 24,
|
|
245
269
|
backgroundColor: 'white',
|
|
@@ -251,6 +275,7 @@ const styles = StyleSheet.create({
|
|
|
251
275
|
shadowRadius: 12,
|
|
252
276
|
elevation: 5,
|
|
253
277
|
width: '95%',
|
|
278
|
+
...(Platform.OS === 'web' ? { maxWidth: 450, paddingVertical: 40 } : {}),
|
|
254
279
|
},
|
|
255
280
|
title: {
|
|
256
281
|
fontSize: 24,
|
|
@@ -266,7 +291,9 @@ const styles = StyleSheet.create({
|
|
|
266
291
|
lineHeight: 24,
|
|
267
292
|
textAlign: 'center',
|
|
268
293
|
},
|
|
269
|
-
contentContainer: {
|
|
294
|
+
contentContainer: {
|
|
295
|
+
width: '100%',
|
|
296
|
+
},
|
|
270
297
|
inputContainer: {
|
|
271
298
|
marginBottom: 24,
|
|
272
299
|
},
|
|
@@ -280,7 +307,7 @@ const styles = StyleSheet.create({
|
|
|
280
307
|
input: {
|
|
281
308
|
borderWidth: 1,
|
|
282
309
|
borderColor: '#e0e0e0',
|
|
283
|
-
padding: 16,
|
|
310
|
+
padding: Platform.OS === 'web' ? 14 : 16,
|
|
284
311
|
borderRadius: 12,
|
|
285
312
|
fontSize: 16,
|
|
286
313
|
backgroundColor: '#f8f9fa',
|
|
@@ -297,10 +324,12 @@ const styles = StyleSheet.create({
|
|
|
297
324
|
alignItems: 'center',
|
|
298
325
|
width: '100%',
|
|
299
326
|
height: '100%',
|
|
327
|
+
...(Platform.OS === 'web' ? { cursor: 'text' } as any : {}),
|
|
300
328
|
},
|
|
301
329
|
otpBox: {
|
|
302
330
|
width: '14%',
|
|
303
|
-
aspectRatio: 1,
|
|
331
|
+
aspectRatio: Platform.OS === 'web' ? undefined : 1,
|
|
332
|
+
height: Platform.OS === 'web' ? 56 : undefined,
|
|
304
333
|
borderWidth: 1,
|
|
305
334
|
borderColor: '#e0e0e0',
|
|
306
335
|
borderRadius: 12,
|
|
@@ -329,6 +358,7 @@ const styles = StyleSheet.create({
|
|
|
329
358
|
width: '100%',
|
|
330
359
|
height: '100%',
|
|
331
360
|
opacity: 0,
|
|
361
|
+
...(Platform.OS === 'web' ? { cursor: 'text' } as any : {}),
|
|
332
362
|
},
|
|
333
363
|
errorText: {
|
|
334
364
|
color: '#dc2626',
|
|
@@ -348,6 +378,7 @@ const styles = StyleSheet.create({
|
|
|
348
378
|
changeEmailLink: {
|
|
349
379
|
alignSelf: 'flex-end',
|
|
350
380
|
marginTop: 12,
|
|
381
|
+
...(Platform.OS === 'web' ? { cursor: 'pointer' } as any : {}),
|
|
351
382
|
},
|
|
352
383
|
changeEmailText: {
|
|
353
384
|
color: '#2DBD60',
|
|
@@ -358,6 +389,7 @@ const styles = StyleSheet.create({
|
|
|
358
389
|
marginTop: 16,
|
|
359
390
|
alignItems: 'center',
|
|
360
391
|
width: "100%",
|
|
392
|
+
...(Platform.OS === 'web' ? { cursor: 'pointer' } as any : {}),
|
|
361
393
|
},
|
|
362
394
|
resendText: {
|
|
363
395
|
color: '#666',
|