@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.
@@ -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
- isCurrent && styles.otpBoxActive,
139
- isFilled && styles.otpBoxFilled
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.container}>
152
- <Text style={styles.title}>{title}</Text>
153
- <Text style={styles.instructions}>
154
- {step === 'email' ? instructions : (t('kyc.enterCodeSent') || `Please enter the code sent to ${email}`)}
155
- </Text>
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
- <View style={styles.contentContainer}>
158
- {step === 'email' ? (
159
- <View style={styles.inputContainer}>
160
- <Text style={styles.label}>{t('common.email') || 'Email'}</Text>
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
- ref={inputRef}
181
- style={styles.hiddenInput}
182
- value={otp}
183
- onChangeText={onChangeOtp}
184
- keyboardType="number-pad"
185
- maxLength={CODE_LENGTH}
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
- <TouchableOpacity onPress={handleBackToEmail} style={styles.changeEmailLink} disabled={isSimulating}>
193
- <Text style={styles.changeEmailText}>{t('common.changeEmail') || 'Change email'}</Text>
194
- </TouchableOpacity>
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
- <Button
203
- title={isSimulating ? (t('common.processing') || 'Processing...') : buttonText}
204
- onPress={step === 'email' ? handleSendCode : handleVerifyCode}
205
- style={styles.button}
206
- disabled={
207
- isSimulating ||
208
- (step === 'email' ? !email : otp.length < CODE_LENGTH)
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
- {step === 'otp' && (
213
- <TouchableOpacity
214
- onPress={async () => {
215
- if (isSimulating) return;
216
- setLocalError(null);
217
- setIsSimulating(true);
218
- try {
219
- await kycService.sendEmailVerificationCode(email.trim(), auth);
220
- Alert.alert(
221
- t('common.codeResent') || 'Code Resent',
222
- t('common.codeResentMessage', { email }) || 'Code resent to ' + email
223
- );
224
- } catch (err: any) {
225
- const msg = errorMessage(err) ?? err?.message ?? (t('errors.sendCodeFailed') || 'Failed to send code');
226
- setLocalError(typeof msg === 'string' ? msg : JSON.stringify(msg));
227
- } finally {
228
- setIsSimulating(false);
229
- }
230
- }}
231
- style={styles.resendButton}
232
- disabled={isSimulating}
233
- >
234
- <Text style={styles.resendText}>{t('common.resendCode') || 'Resend Code'}</Text>
235
- </TouchableOpacity>
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',