@transfergratis/react-native-sdk 0.1.24 → 0.1.26
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/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml +12 -5
- package/android/build/intermediates/aar_main_jar/debug/syncDebugLibJars/classes.jar +0 -0
- package/android/build/intermediates/annotations_typedef_file/debug/extractDebugAnnotations/typedefs.txt +0 -0
- package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +1 -1
- package/android/build/intermediates/incremental/debug-mergeJavaRes/merge-state +0 -0
- package/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt +61 -59
- package/android/build/intermediates/merged_java_res/debug/mergeDebugJavaResource/feature-transfergratis-react-native-sdk.jar +0 -0
- package/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml +12 -5
- package/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin +0 -0
- package/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin +0 -0
- package/android/build/outputs/aar/transfergratis-react-native-sdk-debug.aar +0 -0
- package/android/build/outputs/logs/manifest-merger-debug-report.txt +26 -34
- package/android/src/main/AndroidManifest.xml +22 -7
- package/build/components/EnhancedCameraView.web.d.ts.map +1 -1
- package/build/components/EnhancedCameraView.web.js +76 -21
- package/build/components/EnhancedCameraView.web.js.map +1 -1
- package/build/components/KYCElements/AdditionalDocumentsTemplate.d.ts +12 -0
- package/build/components/KYCElements/AdditionalDocumentsTemplate.d.ts.map +1 -0
- package/build/components/KYCElements/AdditionalDocumentsTemplate.js +283 -0
- package/build/components/KYCElements/AdditionalDocumentsTemplate.js.map +1 -0
- package/build/components/KYCElements/EmailVerificationTemplate.d.ts +12 -0
- package/build/components/KYCElements/EmailVerificationTemplate.d.ts.map +1 -0
- package/build/components/KYCElements/EmailVerificationTemplate.js +212 -0
- package/build/components/KYCElements/EmailVerificationTemplate.js.map +1 -0
- package/build/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/components/KYCElements/IDCardCapture.js +216 -14
- package/build/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/components/KYCElements/OrientationVideoCapture.d.ts +2 -0
- package/build/components/KYCElements/OrientationVideoCapture.d.ts.map +1 -1
- package/build/components/KYCElements/OrientationVideoCapture.js +2 -2
- package/build/components/KYCElements/OrientationVideoCapture.js.map +1 -1
- package/build/components/KYCElements/OrientationVideoCaptureEnhanced.d.ts +2 -0
- package/build/components/KYCElements/OrientationVideoCaptureEnhanced.d.ts.map +1 -1
- package/build/components/KYCElements/OrientationVideoCaptureEnhanced.js +2 -2
- package/build/components/KYCElements/OrientationVideoCaptureEnhanced.js.map +1 -1
- package/build/components/KYCElements/OrientationVideoCaptureFinal.d.ts +2 -0
- package/build/components/KYCElements/OrientationVideoCaptureFinal.d.ts.map +1 -1
- package/build/components/KYCElements/OrientationVideoCaptureFinal.js +2 -2
- package/build/components/KYCElements/OrientationVideoCaptureFinal.js.map +1 -1
- package/build/components/KYCElements/PersonalInformationTemplate.d.ts +12 -0
- package/build/components/KYCElements/PersonalInformationTemplate.d.ts.map +1 -0
- package/build/components/KYCElements/PersonalInformationTemplate.js +120 -0
- package/build/components/KYCElements/PersonalInformationTemplate.js.map +1 -0
- package/build/components/KYCElements/PhoneVerificationTemplate.d.ts +12 -0
- package/build/components/KYCElements/PhoneVerificationTemplate.d.ts.map +1 -0
- package/build/components/KYCElements/PhoneVerificationTemplate.js +185 -0
- package/build/components/KYCElements/PhoneVerificationTemplate.js.map +1 -0
- package/build/components/KYCElements/SelfieCaptureTemplate.d.ts.map +1 -1
- package/build/components/KYCElements/SelfieCaptureTemplate.js +7 -3
- package/build/components/KYCElements/SelfieCaptureTemplate.js.map +1 -1
- package/build/components/KYCElements/WelcomeTemplate.js +2 -1
- package/build/components/KYCElements/WelcomeTemplate.js.map +1 -1
- package/build/components/OverLay/type.d.ts +2 -0
- package/build/components/OverLay/type.d.ts.map +1 -1
- package/build/components/OverLay/type.js.map +1 -1
- package/build/components/TemplateKYCExample.d.ts +10 -0
- package/build/components/TemplateKYCExample.d.ts.map +1 -1
- package/build/components/TemplateKYCExample.js +7 -30
- package/build/components/TemplateKYCExample.js.map +1 -1
- package/build/components/TemplateKYCFlowRefactored.d.ts +12 -0
- package/build/components/TemplateKYCFlowRefactored.d.ts.map +1 -1
- package/build/components/TemplateKYCFlowRefactored.js +25 -3
- package/build/components/TemplateKYCFlowRefactored.js.map +1 -1
- package/build/config/KYCConfig.d.ts +14 -0
- package/build/config/KYCConfig.d.ts.map +1 -0
- package/build/config/KYCConfig.js +26 -0
- package/build/config/KYCConfig.js.map +1 -0
- package/build/config/allowedDomains.d.ts.map +1 -1
- package/build/config/allowedDomains.js +4 -19
- package/build/config/allowedDomains.js.map +1 -1
- package/build/hooks/useOrientationVideo.d.ts +2 -1
- package/build/hooks/useOrientationVideo.d.ts.map +1 -1
- package/build/hooks/useOrientationVideo.js +3 -3
- package/build/hooks/useOrientationVideo.js.map +1 -1
- package/build/hooks/useTemplateKYCFlow.d.ts +18 -1
- package/build/hooks/useTemplateKYCFlow.d.ts.map +1 -1
- package/build/hooks/useTemplateKYCFlow.js +410 -56
- package/build/hooks/useTemplateKYCFlow.js.map +1 -1
- package/build/i18n/en/index.d.ts +42 -0
- package/build/i18n/en/index.d.ts.map +1 -1
- package/build/i18n/en/index.js +44 -2
- package/build/i18n/en/index.js.map +1 -1
- package/build/i18n/fr/index.d.ts +28 -0
- package/build/i18n/fr/index.d.ts.map +1 -1
- package/build/i18n/fr/index.js +30 -2
- package/build/i18n/fr/index.js.map +1 -1
- package/build/i18n/types.d.ts +2 -0
- package/build/i18n/types.d.ts.map +1 -1
- package/build/i18n/types.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/modules/api/CardAuthentification.d.ts +24 -3
- package/build/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/modules/api/CardAuthentification.js +90 -12
- package/build/modules/api/CardAuthentification.js.map +1 -1
- package/build/modules/api/KYCService.d.ts +17 -7
- package/build/modules/api/KYCService.d.ts.map +1 -1
- package/build/modules/api/KYCService.js +125 -37
- package/build/modules/api/KYCService.js.map +1 -1
- package/build/modules/api/SelfieVerification.d.ts +3 -1
- package/build/modules/api/SelfieVerification.d.ts.map +1 -1
- package/build/modules/api/SelfieVerification.js +17 -1
- package/build/modules/api/SelfieVerification.js.map +1 -1
- package/build/modules/api/TemplateService.d.ts +0 -1
- package/build/modules/api/TemplateService.d.ts.map +1 -1
- package/build/modules/api/TemplateService.js +3 -3
- package/build/modules/api/TemplateService.js.map +1 -1
- package/build/modules/camera/VisionCameraModule.web.d.ts.map +1 -1
- package/build/modules/camera/VisionCameraModule.web.js +27 -8
- package/build/modules/camera/VisionCameraModule.web.js.map +1 -1
- package/build/types/KYC.types.d.ts +130 -5
- package/build/types/KYC.types.d.ts.map +1 -1
- package/build/types/KYC.types.js.map +1 -1
- package/build/types/env.types.d.ts +13 -0
- package/build/types/env.types.d.ts.map +1 -0
- package/build/types/env.types.js +2 -0
- package/build/types/env.types.js.map +1 -0
- package/build/utils/cropByObb.d.ts +7 -0
- package/build/utils/cropByObb.d.ts.map +1 -1
- package/build/utils/cropByObb.js +20 -1
- package/build/utils/cropByObb.js.map +1 -1
- package/build/utils/deviceDetection.d.ts +6 -0
- package/build/utils/deviceDetection.d.ts.map +1 -0
- package/build/utils/deviceDetection.js +12 -0
- package/build/utils/deviceDetection.js.map +1 -0
- package/build/utils/platformAlert.d.ts.map +1 -1
- package/build/utils/platformAlert.js.map +1 -1
- package/build/utils/template-transformer.d.ts.map +1 -1
- package/build/utils/template-transformer.js +12 -0
- package/build/utils/template-transformer.js.map +1 -1
- package/build/web/WebKYCEntry.d.ts.map +1 -1
- package/build/web/WebKYCEntry.js +88 -38
- package/build/web/WebKYCEntry.js.map +1 -1
- package/package.json +1 -1
- package/plugin/build/index.d.ts +1 -0
- package/plugin/build/index.js +3 -1
- package/plugin/build/withRemovePermissions.d.ts +3 -0
- package/plugin/build/withRemovePermissions.js +67 -0
- package/plugin/build/withVisionCamera.js +3 -4
- package/plugin/src/index.ts +2 -1
- package/plugin/src/withRemovePermissions.js +85 -0
- package/plugin/src/withRemovePermissions.ts +83 -0
- package/plugin/src/withVisionCamera.js +3 -4
- package/plugin/src/withVisionCamera.ts +3 -4
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/plugin.js +6 -1
- package/src/components/EnhancedCameraView.web.tsx +76 -21
- package/src/components/KYCElements/AdditionalDocumentsTemplate.tsx +346 -0
- package/src/components/KYCElements/EmailVerificationTemplate.tsx +278 -0
- package/src/components/KYCElements/IDCardCapture.tsx +253 -21
- package/src/components/KYCElements/OrientationVideoCapture.tsx +4 -1
- package/src/components/KYCElements/OrientationVideoCaptureEnhanced.tsx +4 -1
- package/src/components/KYCElements/OrientationVideoCaptureFinal.tsx +4 -1
- package/src/components/KYCElements/PersonalInformationTemplate.tsx +158 -0
- package/src/components/KYCElements/PhoneVerificationTemplate.tsx +253 -0
- package/src/components/KYCElements/SelfieCaptureTemplate.tsx +6 -3
- package/src/components/KYCElements/WelcomeTemplate.tsx +2 -1
- package/src/components/OverLay/type.ts +2 -0
- package/src/components/TemplateKYCExample.tsx +35 -46
- package/src/components/TemplateKYCFlowRefactored.tsx +46 -2
- package/src/config/KYCConfig.ts +34 -0
- package/src/config/allowedDomains.ts +7 -26
- package/src/hooks/useOrientationVideo.ts +5 -4
- package/src/hooks/useTemplateKYCFlow.tsx +443 -56
- package/src/i18n/en/index.ts +46 -3
- package/src/i18n/fr/index.ts +31 -2
- package/src/i18n/types.ts +2 -0
- package/src/index.ts +3 -0
- package/src/modules/api/CardAuthentification.ts +98 -12
- package/src/modules/api/KYCService.ts +158 -37
- package/src/modules/api/SelfieVerification.ts +25 -3
- package/src/modules/api/TemplateService.ts +4 -4
- package/src/modules/camera/VisionCameraModule.web.ts +30 -12
- package/src/types/KYC.types.ts +153 -6
- package/src/types/env.types.ts +13 -0
- package/src/utils/cropByObb.ts +20 -1
- package/src/utils/deviceDetection.ts +11 -0
- package/src/utils/platformAlert.ts +1 -0
- package/src/utils/template-transformer.ts +20 -8
- package/src/web/WebKYCEntry.tsx +123 -61
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, Text, StyleSheet, TextInput, TouchableOpacity, Alert } from 'react-native';
|
|
3
|
+
import { TemplateComponent, LocalizedText } from '../../types/KYC.types';
|
|
4
|
+
import { useTemplateKYCFlowContext } from '../../hooks/useTemplateKYCFlow';
|
|
5
|
+
import { useI18n } from '../../hooks/useI18n';
|
|
6
|
+
import { Button } from '../ui/Button';
|
|
7
|
+
import kycService, { errorMessage } from '../../modules/api/KYCService';
|
|
8
|
+
|
|
9
|
+
interface EmailVerificationTemplateProps {
|
|
10
|
+
component: TemplateComponent;
|
|
11
|
+
value?: any;
|
|
12
|
+
onValueChange: (data: any) => void;
|
|
13
|
+
error?: string;
|
|
14
|
+
language?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type VerificationStep = 'email' | 'otp';
|
|
18
|
+
|
|
19
|
+
/** RFC-style email validation: local@domain.tld */
|
|
20
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
21
|
+
|
|
22
|
+
const isValidEmail = (value: string): boolean => EMAIL_REGEX.test((value || '').trim());
|
|
23
|
+
|
|
24
|
+
export const EmailVerificationTemplate: React.FC<EmailVerificationTemplateProps> = ({
|
|
25
|
+
component,
|
|
26
|
+
value,
|
|
27
|
+
onValueChange,
|
|
28
|
+
error: propError,
|
|
29
|
+
}) => {
|
|
30
|
+
const { actions, getLocalizedText, state, apiKey } = useTemplateKYCFlowContext();
|
|
31
|
+
const { t } = useI18n();
|
|
32
|
+
|
|
33
|
+
const auth = apiKey ? { apiKey } : (state.session.token ? { token: state.session.token } : undefined);
|
|
34
|
+
// const config = component.config as EmailVerificationConfig; // Keep for future use
|
|
35
|
+
|
|
36
|
+
// State
|
|
37
|
+
const [step, setStep] = useState<VerificationStep>('email');
|
|
38
|
+
const [email, setEmail] = useState('');
|
|
39
|
+
const [otp, setOtp] = useState('');
|
|
40
|
+
const [localError, setLocalError] = useState<string | null>(null);
|
|
41
|
+
const [isSimulating, setIsSimulating] = useState(false);
|
|
42
|
+
|
|
43
|
+
const title = getLocalizedText(component.labels as LocalizedText);
|
|
44
|
+
const instructions = getLocalizedText(component.instructions as LocalizedText);
|
|
45
|
+
|
|
46
|
+
// Determine button text based on step
|
|
47
|
+
const verifyButtonText = getLocalizedText((component.ui as any).buttonText) || t('common.verify') || 'Verify';
|
|
48
|
+
const sendButtonText = t('common.sendCode') || 'Send Verification Code';
|
|
49
|
+
const buttonText = step === 'email' ? sendButtonText : verifyButtonText;
|
|
50
|
+
|
|
51
|
+
const handleSendCode = async () => {
|
|
52
|
+
const trimmed = email.trim();
|
|
53
|
+
if (!trimmed || !isValidEmail(trimmed)) {
|
|
54
|
+
setLocalError(t('errors.invalidEmail') || 'Please enter a valid email address');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setLocalError(null);
|
|
59
|
+
setIsSimulating(true);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await kycService.sendEmailVerificationCode(trimmed, auth);
|
|
63
|
+
setStep('otp');
|
|
64
|
+
} catch (err: any) {
|
|
65
|
+
const msg = errorMessage(err) ?? err?.message ?? (t('errors.sendCodeFailed') || 'Failed to send verification code');
|
|
66
|
+
setLocalError(typeof msg === 'string' ? msg : JSON.stringify(msg));
|
|
67
|
+
} finally {
|
|
68
|
+
setIsSimulating(false);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleVerifyCode = async () => {
|
|
73
|
+
if (!otp || otp.length < 4) {
|
|
74
|
+
setLocalError(t('errors.invalidCode') || 'Please enter the 6-digit code');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setLocalError(null);
|
|
79
|
+
setIsSimulating(true);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await kycService.verifyEmailCode(otp.trim(), auth);
|
|
83
|
+
const data = { email, otp, verified: true };
|
|
84
|
+
onValueChange(data);
|
|
85
|
+
actions.nextComponent(data);
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
const msg = errorMessage(err) ?? err?.message ?? (t('errors.wrongCode') || 'Invalid verification code');
|
|
88
|
+
setLocalError(typeof msg === 'string' ? msg : JSON.stringify(msg));
|
|
89
|
+
} finally {
|
|
90
|
+
setIsSimulating(false);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const onChangeEmail = (text: string) => {
|
|
95
|
+
setEmail(text);
|
|
96
|
+
if (localError) setLocalError(null);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const onChangeOtp = (text: string) => {
|
|
100
|
+
setOtp(text);
|
|
101
|
+
if (localError) setLocalError(null);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handleBackToEmail = () => {
|
|
105
|
+
setStep('email');
|
|
106
|
+
setOtp('');
|
|
107
|
+
setLocalError(null);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<View style={styles.container}>
|
|
112
|
+
<Text style={styles.title}>{title}</Text>
|
|
113
|
+
<Text style={styles.instructions}>
|
|
114
|
+
{step === 'email' ? instructions : (t('kyc.enterCodeSent') || `Please enter the code sent to ${email}`)}
|
|
115
|
+
</Text>
|
|
116
|
+
|
|
117
|
+
<View style={styles.contentContainer}>
|
|
118
|
+
{step === 'email' ? (
|
|
119
|
+
<View style={styles.inputContainer}>
|
|
120
|
+
<Text style={styles.label}>{t('common.email') || 'Email'}</Text>
|
|
121
|
+
<TextInput
|
|
122
|
+
style={styles.input}
|
|
123
|
+
placeholder="name@example.com"
|
|
124
|
+
value={email}
|
|
125
|
+
onChangeText={onChangeEmail}
|
|
126
|
+
keyboardType="email-address"
|
|
127
|
+
autoCapitalize="none"
|
|
128
|
+
autoCorrect={false}
|
|
129
|
+
editable={!isSimulating}
|
|
130
|
+
/>
|
|
131
|
+
</View>
|
|
132
|
+
) : (
|
|
133
|
+
<View style={styles.inputContainer}>
|
|
134
|
+
<Text style={styles.label}>{t('common.verificationCode') || 'Verification Code'}</Text>
|
|
135
|
+
<TextInput
|
|
136
|
+
style={styles.input}
|
|
137
|
+
placeholder="123456"
|
|
138
|
+
value={otp}
|
|
139
|
+
onChangeText={onChangeOtp}
|
|
140
|
+
keyboardType="number-pad"
|
|
141
|
+
maxLength={6}
|
|
142
|
+
editable={!isSimulating}
|
|
143
|
+
/>
|
|
144
|
+
<TouchableOpacity onPress={handleBackToEmail} style={styles.changeEmailLink}>
|
|
145
|
+
<Text style={styles.changeEmailText}>{t('common.changeEmail') || 'Change email'}</Text>
|
|
146
|
+
</TouchableOpacity>
|
|
147
|
+
</View>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{(localError || propError) && (
|
|
151
|
+
<Text style={styles.errorText}>{localError || propError}</Text>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
<Button
|
|
155
|
+
title={isSimulating ? (t('common.processing') || 'Processing...') : buttonText}
|
|
156
|
+
onPress={step === 'email' ? handleSendCode : handleVerifyCode}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
disabled={
|
|
160
|
+
isSimulating ||
|
|
161
|
+
(step === 'email' ? !email : !otp)
|
|
162
|
+
}
|
|
163
|
+
/>
|
|
164
|
+
|
|
165
|
+
{step === 'otp' && (
|
|
166
|
+
<TouchableOpacity
|
|
167
|
+
onPress={async () => {
|
|
168
|
+
if (isSimulating) return;
|
|
169
|
+
setLocalError(null);
|
|
170
|
+
setIsSimulating(true);
|
|
171
|
+
try {
|
|
172
|
+
await kycService.sendEmailVerificationCode(email.trim(), auth);
|
|
173
|
+
Alert.alert(
|
|
174
|
+
t('common.codeResent') || 'Code Resent',
|
|
175
|
+
t('common.codeResentMessage', { email }) || 'Code resent to ' + email
|
|
176
|
+
);
|
|
177
|
+
} catch (err: any) {
|
|
178
|
+
const msg = errorMessage(err) ?? err?.message ?? (t('errors.sendCodeFailed') || 'Failed to send code');
|
|
179
|
+
setLocalError(typeof msg === 'string' ? msg : JSON.stringify(msg));
|
|
180
|
+
} finally {
|
|
181
|
+
setIsSimulating(false);
|
|
182
|
+
}
|
|
183
|
+
}}
|
|
184
|
+
style={styles.resendButton}
|
|
185
|
+
disabled={isSimulating}
|
|
186
|
+
>
|
|
187
|
+
<Text style={styles.resendText}>{t('common.resendCode') || 'Resend Code'}</Text>
|
|
188
|
+
</TouchableOpacity>
|
|
189
|
+
)}
|
|
190
|
+
</View>
|
|
191
|
+
</View>
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const styles = StyleSheet.create({
|
|
196
|
+
container: {
|
|
197
|
+
padding: 24,
|
|
198
|
+
backgroundColor: 'white',
|
|
199
|
+
borderRadius: 16,
|
|
200
|
+
margin: 16,
|
|
201
|
+
shadowColor: '#000',
|
|
202
|
+
shadowOffset: { width: 0, height: 4 },
|
|
203
|
+
shadowOpacity: 0.1,
|
|
204
|
+
shadowRadius: 12,
|
|
205
|
+
elevation: 5,
|
|
206
|
+
width: '95%',
|
|
207
|
+
},
|
|
208
|
+
title: {
|
|
209
|
+
fontSize: 24,
|
|
210
|
+
fontWeight: '700',
|
|
211
|
+
marginBottom: 8,
|
|
212
|
+
color: '#1a1a1a',
|
|
213
|
+
textAlign: 'center',
|
|
214
|
+
},
|
|
215
|
+
instructions: {
|
|
216
|
+
fontSize: 16,
|
|
217
|
+
color: '#666',
|
|
218
|
+
marginBottom: 32,
|
|
219
|
+
lineHeight: 24,
|
|
220
|
+
textAlign: 'center',
|
|
221
|
+
},
|
|
222
|
+
contentContainer: {
|
|
223
|
+
// width: '100%',
|
|
224
|
+
},
|
|
225
|
+
inputContainer: {
|
|
226
|
+
marginBottom: 24,
|
|
227
|
+
},
|
|
228
|
+
label: {
|
|
229
|
+
fontSize: 14,
|
|
230
|
+
fontWeight: '600',
|
|
231
|
+
color: '#333',
|
|
232
|
+
marginBottom: 8,
|
|
233
|
+
marginLeft: 4,
|
|
234
|
+
},
|
|
235
|
+
input: {
|
|
236
|
+
borderWidth: 1,
|
|
237
|
+
borderColor: '#e0e0e0',
|
|
238
|
+
padding: 16,
|
|
239
|
+
borderRadius: 12,
|
|
240
|
+
fontSize: 16,
|
|
241
|
+
backgroundColor: '#f8f9fa',
|
|
242
|
+
color: '#333',
|
|
243
|
+
},
|
|
244
|
+
errorText: {
|
|
245
|
+
color: '#dc2626',
|
|
246
|
+
marginBottom: 16,
|
|
247
|
+
fontSize: 14,
|
|
248
|
+
textAlign: 'center',
|
|
249
|
+
backgroundColor: '#fee2e2',
|
|
250
|
+
padding: 8,
|
|
251
|
+
borderRadius: 8,
|
|
252
|
+
overflow: 'hidden',
|
|
253
|
+
},
|
|
254
|
+
button: {
|
|
255
|
+
height: 50,
|
|
256
|
+
borderRadius: 12,
|
|
257
|
+
width: "100%"
|
|
258
|
+
},
|
|
259
|
+
changeEmailLink: {
|
|
260
|
+
alignSelf: 'flex-end',
|
|
261
|
+
marginTop: 8,
|
|
262
|
+
},
|
|
263
|
+
changeEmailText: {
|
|
264
|
+
color: '#2DBD60',
|
|
265
|
+
fontSize: 14,
|
|
266
|
+
fontWeight: '500',
|
|
267
|
+
},
|
|
268
|
+
resendButton: {
|
|
269
|
+
marginTop: 16,
|
|
270
|
+
alignItems: 'center',
|
|
271
|
+
width: "100%"
|
|
272
|
+
},
|
|
273
|
+
resendText: {
|
|
274
|
+
color: '#666',
|
|
275
|
+
fontSize: 14,
|
|
276
|
+
textDecorationLine: 'underline',
|
|
277
|
+
},
|
|
278
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import { View, Text, StyleSheet, Image, ScrollView } from 'react-native';
|
|
2
|
+
import { View, Text, StyleSheet, Image, ScrollView, Platform, Modal, TouchableOpacity, ActivityIndicator } from 'react-native';
|
|
3
3
|
import { showAlert } from '../../utils/platformAlert';
|
|
4
4
|
import { EnhancedCameraView } from '../EnhancedCameraView';
|
|
5
5
|
import { TemplateComponent, LocalizedText, GovernmentDocumentType, ISilentCaptureResult, IBbox, GovernmentDocumentTypeShorted, GovernmentDocumentTypeBackend } from '../../types/KYC.types';
|
|
@@ -11,8 +11,9 @@ import { removeDuplicates } from '../../utils/remove-duplicate';
|
|
|
11
11
|
import { backVerification, checkTemplateType, frontVerification } from '../../modules/api/CardAuthentification';
|
|
12
12
|
import { getDocumentTypeInfo } from '../../utils/get-document-type-info';
|
|
13
13
|
import pathToBase64 from '../../utils/pathToBase64';
|
|
14
|
-
import { truncateFields } from '../../modules/api/KYCService';
|
|
15
|
-
import { cropByObb, cropImageWithBBox, cropImageWithBBoxWithTolerance } from '../../utils/cropByObb';
|
|
14
|
+
import kycService, { truncateFields } from '../../modules/api/KYCService';
|
|
15
|
+
import { cropByObb, cropImageWithBBox, cropImageWithBBoxWithTolerance, getObbConfidence, OBB_CONFIDENCE_THRESHOLD } from '../../utils/cropByObb';
|
|
16
|
+
import { isMobileWeb } from '../../utils/deviceDetection';
|
|
16
17
|
import { logger } from '../../utils/logger';
|
|
17
18
|
|
|
18
19
|
|
|
@@ -47,6 +48,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
47
48
|
// Stocker les bbox par côté pour pouvoir restaurer les images croppées
|
|
48
49
|
const [bboxBySide, setBboxBySide] = useState<Record<string, IBbox>>({});
|
|
49
50
|
const [silentCaptureResult, setSilentCaptureResult] = useState<ISilentCaptureResult>({ success: false, isAnalyzing: false });
|
|
51
|
+
const [showQRModal, setShowQRModal] = useState(false);
|
|
50
52
|
|
|
51
53
|
// Mapping des types de documents backend vers SDK
|
|
52
54
|
const documentTypeMapping: Record<string, GovernmentDocumentType> = {
|
|
@@ -58,7 +60,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
58
60
|
};
|
|
59
61
|
// const [imageNaturalSize, setImageNaturalSize] = useState<{ width: number; height: number } | null>(null);
|
|
60
62
|
|
|
61
|
-
const { actions, state } = useTemplateKYCFlowContext();
|
|
63
|
+
const { actions, state, env } = useTemplateKYCFlowContext();
|
|
62
64
|
|
|
63
65
|
|
|
64
66
|
|
|
@@ -96,6 +98,35 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
96
98
|
}, [countrySelectionData]);
|
|
97
99
|
|
|
98
100
|
|
|
101
|
+
// Synchroniser capturedImages avec value quand les données sont chargées (ex: reprise de session)
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (value && Object.keys(value).length > 0) {
|
|
104
|
+
// Vérifier si les données ont changé
|
|
105
|
+
const valueChanged = JSON.stringify(value) !== JSON.stringify(capturedImages);
|
|
106
|
+
if (valueChanged) {
|
|
107
|
+
logger.log("Updating capturedImages from value:", Object.keys(value));
|
|
108
|
+
logger.log("Value data sample:", truncateFields(value));
|
|
109
|
+
const updatedImages = value as Record<string, IIDCardPayload>;
|
|
110
|
+
setCapturedImages(updatedImages);
|
|
111
|
+
|
|
112
|
+
// Si on a des images chargées, mettre à jour silentCaptureResult pour l'affichage
|
|
113
|
+
Object.keys(updatedImages).forEach((side) => {
|
|
114
|
+
const imageData = updatedImages[side];
|
|
115
|
+
if (imageData?.dir) {
|
|
116
|
+
setSilentCaptureResult(prev => ({
|
|
117
|
+
...prev,
|
|
118
|
+
path: imageData.dir,
|
|
119
|
+
success: true,
|
|
120
|
+
isAnalyzing: false,
|
|
121
|
+
mrz: imageData.mrz || '',
|
|
122
|
+
templatePath: imageData.templatePath || '',
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}, [value]);
|
|
129
|
+
|
|
99
130
|
useEffect(() => {
|
|
100
131
|
logger.log("cropImageUri", JSON.stringify(truncateFields({ box: silentCaptureResult }), null, 2));
|
|
101
132
|
if (capturedImages[currentSide]?.dir && silentCaptureResult?.bbox) {
|
|
@@ -213,7 +244,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
213
244
|
countrySelectionDataDocumentType: countrySelectionData?.documentType,
|
|
214
245
|
docTypeToSend: selectedDocumentType?.type
|
|
215
246
|
});
|
|
216
|
-
const templateType = await checkTemplateType({ path: result.path || '', docType: selectedDocumentType?.type as GovernmentDocumentType, docRegion: countryData?.code || "", postfix: currentSide });
|
|
247
|
+
const templateType = await checkTemplateType({ path: result.path || '', docType: selectedDocumentType?.type as GovernmentDocumentType, docRegion: countryData?.code || "", postfix: currentSide }, env);
|
|
217
248
|
|
|
218
249
|
if (templateType.template_path) {
|
|
219
250
|
templatePath = templateType.template_path;
|
|
@@ -221,6 +252,16 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
221
252
|
setSilentCaptureResult((prev) => ({ ...prev, templatePath: templatePath }));
|
|
222
253
|
}
|
|
223
254
|
if (templateType.card_obb) {
|
|
255
|
+
const obbConfidence = getObbConfidence((templateType as any).card_obb);
|
|
256
|
+
if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
|
|
257
|
+
setSilentCaptureResult((prev) => ({
|
|
258
|
+
...prev,
|
|
259
|
+
isAnalyzing: false,
|
|
260
|
+
success: false,
|
|
261
|
+
error: t('kyc.idCardCapture.cardNotFullyInFrame'),
|
|
262
|
+
}));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
224
265
|
let bbox: IBbox | undefined;
|
|
225
266
|
try {
|
|
226
267
|
const crop = await cropByObb(result?.path || '', (templateType as any).card_obb);
|
|
@@ -260,7 +301,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
260
301
|
};
|
|
261
302
|
console.log("frontVerification params", verificationParams);
|
|
262
303
|
console.log("About to call frontVerification function");
|
|
263
|
-
const promise = frontVerification(verificationParams);
|
|
304
|
+
const promise = frontVerification(verificationParams, env);
|
|
264
305
|
console.log("frontVerification promise created", promise);
|
|
265
306
|
promise.then((mrz) => {
|
|
266
307
|
logger.log("front verification result", truncateFields(mrz));
|
|
@@ -283,8 +324,9 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
283
324
|
}).catch((e: any) => {
|
|
284
325
|
console.log("error front verification", e);
|
|
285
326
|
logger.log("error front verification", truncateFields(e));
|
|
286
|
-
|
|
287
|
-
|
|
327
|
+
const isCardNotFullyInFrame = e?.message?.includes('entirement') || e?.message?.includes('fully in frame');
|
|
328
|
+
const errorMessage = isCardNotFullyInFrame ? t('kyc.idCardCapture.cardNotFullyInFrame') : (e?.message || 'Erreur de détection du MRZ');
|
|
329
|
+
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, templatePath: templatePath, success: false, error: errorMessage }));
|
|
288
330
|
});
|
|
289
331
|
} catch (error: any) {
|
|
290
332
|
console.log("Error setting up frontVerification call", error);
|
|
@@ -304,7 +346,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
304
346
|
currentSide: currentSide,
|
|
305
347
|
templatePath: templatePath,
|
|
306
348
|
mrzType: getCorrespondingMrzType(templatePath, backRegionMappings.regionMapping, backRegionMappings.key || '') || '',
|
|
307
|
-
}).then((mrz) => {
|
|
349
|
+
}, env).then((mrz) => {
|
|
308
350
|
logger.log("back verification result", truncateFields(mrz));
|
|
309
351
|
const bbox = (mrz as any)?.bbox || templateBbox;
|
|
310
352
|
setSilentCaptureResult((prev) => ({
|
|
@@ -320,8 +362,9 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
320
362
|
}
|
|
321
363
|
}).catch((e: any) => {
|
|
322
364
|
logger.log("error back verification", truncateFields(e));
|
|
323
|
-
|
|
324
|
-
|
|
365
|
+
const isCardNotFullyInFrame = e?.message?.includes('entirement') || e?.message?.includes('fully in frame');
|
|
366
|
+
const errorMessage = isCardNotFullyInFrame ? t('kyc.idCardCapture.cardNotFullyInFrame') : (e?.message || 'Erreur de détection du MRZ');
|
|
367
|
+
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, templatePath: templatePath, success: false, error: errorMessage }));
|
|
325
368
|
})
|
|
326
369
|
}
|
|
327
370
|
|
|
@@ -380,12 +423,82 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
380
423
|
actions.showCustomStepper(!showCamera);
|
|
381
424
|
}, [showCamera]);
|
|
382
425
|
|
|
426
|
+
// Cross-device polling logic
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
if (!showQRModal || !state.session.session_id) return;
|
|
429
|
+
|
|
430
|
+
const pollInterval = setInterval(async () => {
|
|
431
|
+
try {
|
|
432
|
+
const result = await kycService.getVerificationResult(state.session.session_id);
|
|
433
|
+
const sessionData = result[state.session.session_id]?.data;
|
|
434
|
+
|
|
435
|
+
if (sessionData) {
|
|
436
|
+
// Check if verification is completed or if we have ID card data
|
|
437
|
+
// Since the requirement is "verification restarts", we might look for overall completion
|
|
438
|
+
// or we could check if user_data is populated.
|
|
439
|
+
// For now, let's assume if status is not PENDING/INITIALIZED it might be done.
|
|
440
|
+
// Or simplier: if the mobile flow completes, the session status updates.
|
|
441
|
+
logger.log('Polling result:', truncateFields(sessionData));
|
|
442
|
+
|
|
443
|
+
if (sessionData.verification_status === 'completed' || sessionData.verification_status === 'approved' || sessionData.verification_status === 'review') {
|
|
444
|
+
clearInterval(pollInterval);
|
|
445
|
+
setShowQRModal(false);
|
|
446
|
+
actions.submitVerification(); // Or handleComplete
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.error('Polling error:', error);
|
|
451
|
+
}
|
|
452
|
+
}, 5000);
|
|
453
|
+
|
|
454
|
+
return () => clearInterval(pollInterval);
|
|
455
|
+
}, [showQRModal, state.session.session_id, actions]);
|
|
456
|
+
|
|
457
|
+
const getQrCodeUrl = (): string => {
|
|
458
|
+
// Only available on web platform
|
|
459
|
+
if (Platform.OS !== 'web') return '';
|
|
460
|
+
if (typeof window === 'undefined' || !window.location || !window.location.href) return '';
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
const currentUrl = new URL(window.location.href);
|
|
464
|
+
if (!currentUrl.searchParams.has('kyc_id') && state.session.session_id) {
|
|
465
|
+
currentUrl.searchParams.set('kyc_id', state.session.session_id);
|
|
466
|
+
}
|
|
467
|
+
currentUrl.searchParams.set('component_index', String(state.currentComponentIndex));
|
|
468
|
+
if (countrySelectionData?.code) {
|
|
469
|
+
currentUrl.searchParams.set('country', countrySelectionData.code);
|
|
470
|
+
if (countrySelectionData.documentType) currentUrl.searchParams.set('document_type', countrySelectionData.documentType);
|
|
471
|
+
if (countrySelectionData.region) currentUrl.searchParams.set('region', countrySelectionData.region);
|
|
472
|
+
}
|
|
473
|
+
return `https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${encodeURIComponent(currentUrl.toString())}`;
|
|
474
|
+
} catch (error) {
|
|
475
|
+
console.warn('Error generating QR code URL:', error);
|
|
476
|
+
return '';
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
383
480
|
|
|
384
481
|
|
|
385
482
|
|
|
386
483
|
|
|
387
484
|
|
|
388
485
|
|
|
486
|
+
// En reprise sur un autre appareil: afficher un chargement tant que les données de session ne sont pas restaurées
|
|
487
|
+
const isResumingSession = Boolean(state.session.session_id && state.currentComponentIndex > 0);
|
|
488
|
+
const sessionDataRestored = state.session.sessionDataRestored !== false;
|
|
489
|
+
if (isResumingSession && !sessionDataRestored && (!countrySelectionData || !selectedDocumentType)) {
|
|
490
|
+
return (
|
|
491
|
+
<View style={styles.root}>
|
|
492
|
+
<View style={[styles.container, { justifyContent: 'center', alignItems: 'center' }]}>
|
|
493
|
+
<ActivityIndicator size="large" color="#2DBD60" />
|
|
494
|
+
<Text style={[styles.description, { marginTop: 16 }]}>
|
|
495
|
+
{state.currentLanguage === 'en' ? 'Loading your session...' : 'Chargement de votre session...'}
|
|
496
|
+
</Text>
|
|
497
|
+
</View>
|
|
498
|
+
</View>
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
389
502
|
// Vérifier si les données sont disponibles, sinon afficher un message d'erreur
|
|
390
503
|
if (!countrySelectionData || !selectedDocumentType) {
|
|
391
504
|
return (
|
|
@@ -423,6 +536,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
423
536
|
showSwitchCamera={true}
|
|
424
537
|
onSilentCapture={handleSilentCapture}
|
|
425
538
|
silentCaptureResult={silentCaptureResult}
|
|
539
|
+
captureStabilizationDelayMs={3000}
|
|
426
540
|
enableFlash={cameraConfig.flashMode === 'auto' || cameraConfig.flashMode === 'on'}
|
|
427
541
|
overlayComponent={<IdCardOverlay
|
|
428
542
|
xMin={cameraConfig.overlay.bbox.xMin}
|
|
@@ -518,15 +632,28 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
518
632
|
}}
|
|
519
633
|
/>
|
|
520
634
|
) : null}
|
|
521
|
-
{silentCaptureResult.path ? (
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
635
|
+
{!cropImageUri && silentCaptureResult.path ? (
|
|
636
|
+
<Image
|
|
637
|
+
source={{ uri: silentCaptureResult.path }}
|
|
638
|
+
style={{
|
|
639
|
+
width: '100%',
|
|
640
|
+
height: 200,
|
|
641
|
+
borderRadius: 12,
|
|
642
|
+
resizeMode: 'cover',
|
|
643
|
+
}}
|
|
644
|
+
/>
|
|
645
|
+
) : null}
|
|
646
|
+
{!cropImageUri && !silentCaptureResult.path && capturedImages[currentSide]?.dir ? (
|
|
647
|
+
<Image
|
|
648
|
+
source={{ uri: capturedImages[currentSide].dir }}
|
|
649
|
+
style={{
|
|
650
|
+
width: '100%',
|
|
651
|
+
height: 200,
|
|
652
|
+
borderRadius: 12,
|
|
653
|
+
resizeMode: 'cover',
|
|
654
|
+
}}
|
|
655
|
+
/>
|
|
656
|
+
) : null}
|
|
530
657
|
|
|
531
658
|
</View>
|
|
532
659
|
{/* Capture button si aucune image n'a été capturée */}
|
|
@@ -582,6 +709,49 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
582
709
|
{error && (
|
|
583
710
|
<Text style={styles.errorText}>{error}</Text>
|
|
584
711
|
)}
|
|
712
|
+
|
|
713
|
+
{/* Cross-Device / Continue on Phone Button (Web Only) */}
|
|
714
|
+
{Platform.OS === 'web' && !isMobileWeb() && !capturedImages[currentSide]?.dir && (
|
|
715
|
+
<Button title={t('kyc.idCardCapture.continueOnPhone')} onPress={() => { setShowQRModal(true) }} />
|
|
716
|
+
)}
|
|
717
|
+
|
|
718
|
+
{/* QR Code Modal - Web Only */}
|
|
719
|
+
{Platform.OS === 'web' && (
|
|
720
|
+
<Modal
|
|
721
|
+
visible={showQRModal}
|
|
722
|
+
transparent={true}
|
|
723
|
+
animationType="fade"
|
|
724
|
+
onRequestClose={() => setShowQRModal(false)}
|
|
725
|
+
>
|
|
726
|
+
<View style={styles.modalOverlay}>
|
|
727
|
+
<View style={styles.modalContent}>
|
|
728
|
+
<Text style={styles.modalTitle}>
|
|
729
|
+
{t('kyc.idCardCapture.continueOnMobile')}
|
|
730
|
+
</Text>
|
|
731
|
+
<Text style={styles.modalDescription}>
|
|
732
|
+
{t('kyc.idCardCapture.scanQrCode')}
|
|
733
|
+
</Text>
|
|
734
|
+
|
|
735
|
+
{showQRModal && getQrCodeUrl() ? (
|
|
736
|
+
<Image
|
|
737
|
+
source={{ uri: getQrCodeUrl() }}
|
|
738
|
+
style={styles.qrCodeImage}
|
|
739
|
+
/>
|
|
740
|
+
) : null}
|
|
741
|
+
|
|
742
|
+
<TouchableOpacity
|
|
743
|
+
style={styles.closeButton}
|
|
744
|
+
onPress={() => setShowQRModal(false)}
|
|
745
|
+
>
|
|
746
|
+
<Text style={styles.closeButtonText}>
|
|
747
|
+
{t('common.close')}
|
|
748
|
+
</Text>
|
|
749
|
+
</TouchableOpacity>
|
|
750
|
+
</View>
|
|
751
|
+
</View>
|
|
752
|
+
</Modal>
|
|
753
|
+
)}
|
|
754
|
+
|
|
585
755
|
</View>
|
|
586
756
|
</View>
|
|
587
757
|
);
|
|
@@ -613,7 +783,7 @@ const styles = StyleSheet.create({
|
|
|
613
783
|
height: '100%',
|
|
614
784
|
},
|
|
615
785
|
previewContainer: {
|
|
616
|
-
width: '
|
|
786
|
+
width: '95%',
|
|
617
787
|
backgroundColor: 'white',
|
|
618
788
|
margin: 10,
|
|
619
789
|
borderRadius: 10,
|
|
@@ -738,4 +908,66 @@ const styles = StyleSheet.create({
|
|
|
738
908
|
marginTop: 8,
|
|
739
909
|
textAlign: 'center',
|
|
740
910
|
},
|
|
911
|
+
crossDeviceButton: {
|
|
912
|
+
marginTop: 16,
|
|
913
|
+
padding: 12,
|
|
914
|
+
alignItems: 'center',
|
|
915
|
+
borderWidth: 1,
|
|
916
|
+
borderColor: '#2DBD60',
|
|
917
|
+
borderRadius: 8,
|
|
918
|
+
backgroundColor: '#f0f9f0',
|
|
919
|
+
},
|
|
920
|
+
crossDeviceText: {
|
|
921
|
+
color: '#2DBD60',
|
|
922
|
+
fontSize: 16,
|
|
923
|
+
fontWeight: '600',
|
|
924
|
+
},
|
|
925
|
+
modalOverlay: {
|
|
926
|
+
flex: 1,
|
|
927
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
928
|
+
justifyContent: 'center',
|
|
929
|
+
alignItems: 'center',
|
|
930
|
+
},
|
|
931
|
+
modalContent: {
|
|
932
|
+
backgroundColor: 'white',
|
|
933
|
+
borderRadius: 16,
|
|
934
|
+
padding: 24,
|
|
935
|
+
alignItems: 'center',
|
|
936
|
+
width: '90%',
|
|
937
|
+
maxWidth: 340,
|
|
938
|
+
shadowColor: '#000',
|
|
939
|
+
shadowOffset: { width: 0, height: 2 },
|
|
940
|
+
shadowOpacity: 0.25,
|
|
941
|
+
shadowRadius: 4,
|
|
942
|
+
elevation: 5,
|
|
943
|
+
},
|
|
944
|
+
modalTitle: {
|
|
945
|
+
fontSize: 20,
|
|
946
|
+
fontWeight: 'bold',
|
|
947
|
+
marginBottom: 12,
|
|
948
|
+
color: '#333',
|
|
949
|
+
},
|
|
950
|
+
modalDescription: {
|
|
951
|
+
fontSize: 16,
|
|
952
|
+
color: '#666',
|
|
953
|
+
textAlign: 'center',
|
|
954
|
+
marginBottom: 20,
|
|
955
|
+
lineHeight: 22,
|
|
956
|
+
},
|
|
957
|
+
qrCodeImage: {
|
|
958
|
+
width: 200,
|
|
959
|
+
height: 200,
|
|
960
|
+
marginBottom: 20,
|
|
961
|
+
},
|
|
962
|
+
closeButton: {
|
|
963
|
+
paddingVertical: 10,
|
|
964
|
+
paddingHorizontal: 20,
|
|
965
|
+
backgroundColor: '#f5f5f5',
|
|
966
|
+
borderRadius: 8,
|
|
967
|
+
},
|
|
968
|
+
closeButtonText: {
|
|
969
|
+
fontSize: 16,
|
|
970
|
+
color: '#666',
|
|
971
|
+
fontWeight: '600',
|
|
972
|
+
},
|
|
741
973
|
});
|