@transfergratis/react-native-sdk 0.1.22 → 0.1.24
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/src/main/AndroidManifest.xml +9 -4
- package/build/components/EnhancedCameraView.d.ts.map +1 -1
- package/build/components/EnhancedCameraView.js +26 -3
- package/build/components/EnhancedCameraView.js.map +1 -1
- package/build/components/EnhancedCameraView.web.d.ts.map +1 -1
- package/build/components/EnhancedCameraView.web.js +21 -0
- package/build/components/EnhancedCameraView.web.js.map +1 -1
- package/build/components/KYCElements/CameraCapture.d.ts.map +1 -1
- package/build/components/KYCElements/CameraCapture.js +4 -3
- package/build/components/KYCElements/CameraCapture.js.map +1 -1
- package/build/components/KYCElements/CountrySelectionTemplate.d.ts +5 -2
- package/build/components/KYCElements/CountrySelectionTemplate.d.ts.map +1 -1
- package/build/components/KYCElements/CountrySelectionTemplate.js +360 -101
- package/build/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
- package/build/components/KYCElements/FileUpload.d.ts.map +1 -1
- package/build/components/KYCElements/FileUpload.js +5 -4
- package/build/components/KYCElements/FileUpload.js.map +1 -1
- package/build/components/KYCElements/FileUploadTemplate.d.ts.map +1 -1
- package/build/components/KYCElements/FileUploadTemplate.js +5 -4
- package/build/components/KYCElements/FileUploadTemplate.js.map +1 -1
- package/build/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/components/KYCElements/IDCardCapture.js +193 -237
- package/build/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/components/KYCElements/LocationCaptureTemplate.d.ts.map +1 -1
- package/build/components/KYCElements/LocationCaptureTemplate.js +78 -37
- package/build/components/KYCElements/LocationCaptureTemplate.js.map +1 -1
- package/build/components/KYCElements/OrientationVideoCapture.js +3 -2
- package/build/components/KYCElements/OrientationVideoCapture.js.map +1 -1
- package/build/components/KYCElements/OrientationVideoCaptureEnhanced.js +3 -2
- package/build/components/KYCElements/OrientationVideoCaptureEnhanced.js.map +1 -1
- package/build/components/KYCElements/OrientationVideoCaptureFinal.js +3 -2
- package/build/components/KYCElements/OrientationVideoCaptureFinal.js.map +1 -1
- package/build/components/KYCElements/SelfieCapture.d.ts.map +1 -1
- package/build/components/KYCElements/SelfieCapture.js +4 -3
- package/build/components/KYCElements/SelfieCapture.js.map +1 -1
- package/build/components/KYCElements/SelfieCaptureTemplate.d.ts.map +1 -1
- package/build/components/KYCElements/SelfieCaptureTemplate.js +182 -39
- package/build/components/KYCElements/SelfieCaptureTemplate.js.map +1 -1
- package/build/components/KYCElements/WelcomeTemplate.d.ts +12 -0
- package/build/components/KYCElements/WelcomeTemplate.d.ts.map +1 -0
- package/build/components/KYCElements/WelcomeTemplate.js +243 -0
- package/build/components/KYCElements/WelcomeTemplate.js.map +1 -0
- package/build/components/TemplateKYCExample.d.ts +4 -2
- package/build/components/TemplateKYCExample.d.ts.map +1 -1
- package/build/components/TemplateKYCExample.js +5 -68
- package/build/components/TemplateKYCExample.js.map +1 -1
- package/build/components/TemplateKYCFlowRefactored.d.ts +2 -1
- package/build/components/TemplateKYCFlowRefactored.d.ts.map +1 -1
- package/build/components/TemplateKYCFlowRefactored.js +95 -9
- package/build/components/TemplateKYCFlowRefactored.js.map +1 -1
- package/build/components/example/DynamicTemplateExample.d.ts +10 -0
- package/build/components/example/DynamicTemplateExample.d.ts.map +1 -0
- package/build/components/example/DynamicTemplateExample.js +241 -0
- package/build/components/example/DynamicTemplateExample.js.map +1 -0
- package/build/config/allowedDomains.d.ts +30 -0
- package/build/config/allowedDomains.d.ts.map +1 -0
- package/build/config/allowedDomains.js +127 -0
- package/build/config/allowedDomains.js.map +1 -0
- package/build/hooks/useTemplateKYCFlow.d.ts.map +1 -1
- package/build/hooks/useTemplateKYCFlow.js +68 -43
- package/build/hooks/useTemplateKYCFlow.js.map +1 -1
- package/build/hooks/useTemplateLoader.d.ts +14 -0
- package/build/hooks/useTemplateLoader.d.ts.map +1 -0
- package/build/hooks/useTemplateLoader.js +85 -0
- package/build/hooks/useTemplateLoader.js.map +1 -0
- package/build/i18n/en/index.d.ts +9 -0
- package/build/i18n/en/index.d.ts.map +1 -1
- package/build/i18n/en/index.js +9 -0
- package/build/i18n/en/index.js.map +1 -1
- package/build/i18n/fr/index.d.ts +9 -0
- package/build/i18n/fr/index.d.ts.map +1 -1
- package/build/i18n/fr/index.js +9 -0
- package/build/i18n/fr/index.js.map +1 -1
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +8 -0
- package/build/index.js.map +1 -1
- package/build/modules/api/CardAuthentification.js +1 -0
- package/build/modules/api/CardAuthentification.js.map +1 -1
- package/build/modules/api/KYCService.d.ts +4 -1
- package/build/modules/api/KYCService.d.ts.map +1 -1
- package/build/modules/api/KYCService.js +17 -5
- package/build/modules/api/KYCService.js.map +1 -1
- package/build/modules/api/TemplateService.d.ts +45 -0
- package/build/modules/api/TemplateService.d.ts.map +1 -0
- package/build/modules/api/TemplateService.js +145 -0
- package/build/modules/api/TemplateService.js.map +1 -0
- package/build/modules/api/types.d.ts +1 -0
- package/build/modules/api/types.d.ts.map +1 -1
- package/build/modules/api/types.js.map +1 -1
- package/build/types/KYC.types.d.ts +144 -4
- package/build/types/KYC.types.d.ts.map +1 -1
- package/build/types/KYC.types.js +15 -0
- package/build/types/KYC.types.js.map +1 -1
- package/build/utils/cropByObb.d.ts +1 -0
- package/build/utils/cropByObb.d.ts.map +1 -1
- package/build/utils/cropByObb.js +70 -0
- package/build/utils/cropByObb.js.map +1 -1
- package/build/utils/platformAlert.d.ts +20 -0
- package/build/utils/platformAlert.d.ts.map +1 -0
- package/build/utils/platformAlert.js +67 -0
- package/build/utils/platformAlert.js.map +1 -0
- package/build/utils/template-transformer.d.ts +10 -0
- package/build/utils/template-transformer.d.ts.map +1 -0
- package/build/utils/template-transformer.js +353 -0
- package/build/utils/template-transformer.js.map +1 -0
- package/build/web/WebKYCEntry.d.ts.map +1 -1
- package/build/web/WebKYCEntry.js +102 -20
- package/build/web/WebKYCEntry.js.map +1 -1
- package/package.json +1 -1
- package/src/components/EnhancedCameraView.tsx +31 -2
- package/src/components/EnhancedCameraView.web.tsx +24 -0
- package/src/components/KYCElements/CameraCapture.tsx +4 -3
- package/src/components/KYCElements/CountrySelectionTemplate.tsx +410 -113
- package/src/components/KYCElements/FileUpload.tsx +5 -4
- package/src/components/KYCElements/FileUploadTemplate.tsx +5 -4
- package/src/components/KYCElements/IDCardCapture.tsx +196 -254
- package/src/components/KYCElements/LocationCaptureTemplate.tsx +95 -44
- package/src/components/KYCElements/OrientationVideoCapture.tsx +2 -2
- package/src/components/KYCElements/OrientationVideoCaptureEnhanced.tsx +2 -2
- package/src/components/KYCElements/OrientationVideoCaptureFinal.tsx +2 -2
- package/src/components/KYCElements/SelfieCapture.tsx +4 -3
- package/src/components/KYCElements/SelfieCaptureTemplate.tsx +195 -41
- package/src/components/KYCElements/WelcomeTemplate.tsx +289 -0
- package/src/components/TemplateKYCExample.tsx +16 -71
- package/src/components/TemplateKYCFlowRefactored.tsx +121 -11
- package/src/components/example/DynamicTemplateExample.tsx +289 -0
- package/src/config/allowedDomains.ts +152 -0
- package/src/hooks/useTemplateKYCFlow.tsx +71 -46
- package/src/hooks/useTemplateLoader.ts +102 -0
- package/src/i18n/en/index.ts +10 -0
- package/src/i18n/fr/index.ts +9 -0
- package/src/index.ts +11 -0
- package/src/modules/api/CardAuthentification.ts +1 -1
- package/src/modules/api/KYCService.ts +18 -8
- package/src/modules/api/TemplateService.ts +167 -0
- package/src/modules/api/types.ts +1 -0
- package/src/types/KYC.types.ts +188 -3
- package/src/utils/cropByObb.ts +83 -3
- package/src/utils/platformAlert.ts +85 -0
- package/src/utils/template-transformer.ts +433 -0
- package/src/web/WebKYCEntry.tsx +122 -24
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BackendKYCTemplate,
|
|
3
|
+
BackendTemplateComponent,
|
|
4
|
+
KYCTemplate,
|
|
5
|
+
TemplateComponent,
|
|
6
|
+
LocalizedText,
|
|
7
|
+
ComponentUI,
|
|
8
|
+
IDCardConfig,
|
|
9
|
+
LocationConfig,
|
|
10
|
+
SelfieConfig,
|
|
11
|
+
FileUploadConfig,
|
|
12
|
+
CountrySelectionConfig,
|
|
13
|
+
ComponentConfig,
|
|
14
|
+
WelcomeConfig,
|
|
15
|
+
GovernmentDocumentType,
|
|
16
|
+
GovernmentIdConfig,
|
|
17
|
+
} from '../types/KYC.types';
|
|
18
|
+
import { logger } from './logger';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hash a string to a number (simple hash function)
|
|
22
|
+
*/
|
|
23
|
+
function hashStringToNumber(str: string): number {
|
|
24
|
+
let hash = 0;
|
|
25
|
+
for (let i = 0; i < str.length; i++) {
|
|
26
|
+
const char = str.charCodeAt(i);
|
|
27
|
+
hash = ((hash << 5) - hash) + char;
|
|
28
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
29
|
+
}
|
|
30
|
+
return Math.abs(hash);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Map backend component types to SDK component types
|
|
35
|
+
*/
|
|
36
|
+
const COMPONENT_TYPE_MAPPING: Record<string, TemplateComponent['type']> = {
|
|
37
|
+
'welcome': 'welcome',
|
|
38
|
+
'government-id': 'id_card',
|
|
39
|
+
'selfie-capture': 'selfie',
|
|
40
|
+
'location-capture': 'location',
|
|
41
|
+
'review-submit': 'review_submit',
|
|
42
|
+
'verification-progress': 'verification_progress',
|
|
43
|
+
'file-upload': 'file_upload',
|
|
44
|
+
'country-selection': 'country_selection',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Transform backend translations to SDK labels and instructions
|
|
49
|
+
*/
|
|
50
|
+
function transformTranslations(
|
|
51
|
+
translations: BackendTemplateComponent['translations'],
|
|
52
|
+
defaultLanguage: string = 'en'
|
|
53
|
+
): { labels: LocalizedText; instructions: LocalizedText } {
|
|
54
|
+
const labels: LocalizedText = { en: '', fr: '' };
|
|
55
|
+
const instructions: LocalizedText = { en: '', fr: '' };
|
|
56
|
+
|
|
57
|
+
// Extract title as label
|
|
58
|
+
if (translations.en?.title) labels.en = translations.en.title;
|
|
59
|
+
if (translations.fr?.title) labels.fr = translations.fr.title;
|
|
60
|
+
|
|
61
|
+
// Extract instructions
|
|
62
|
+
if (translations.en?.instructions) instructions.en = translations.en.instructions;
|
|
63
|
+
if (translations.fr?.instructions) instructions.fr = translations.fr.instructions;
|
|
64
|
+
|
|
65
|
+
// Fallback to description if instructions not available
|
|
66
|
+
if (!instructions.en && translations.en?.description) {
|
|
67
|
+
instructions.en = translations.en.description;
|
|
68
|
+
}
|
|
69
|
+
if (!instructions.fr && translations.fr?.description) {
|
|
70
|
+
instructions.fr = translations.fr.description;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Support other languages
|
|
74
|
+
Object.keys(translations).forEach((lang) => {
|
|
75
|
+
if (lang !== 'en' && lang !== 'fr' && translations[lang]) {
|
|
76
|
+
labels[lang] = translations[lang]?.title || '';
|
|
77
|
+
instructions[lang] = translations[lang]?.instructions || translations[lang]?.description || '';
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return { labels, instructions };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Transform UI configuration
|
|
86
|
+
*/
|
|
87
|
+
function transformUI(translations: BackendTemplateComponent['translations']): ComponentUI {
|
|
88
|
+
const buttonText: LocalizedText = { en: '', fr: '' };
|
|
89
|
+
|
|
90
|
+
if (translations.en?.buttonText) buttonText.en = translations.en.buttonText;
|
|
91
|
+
if (translations.fr?.buttonText) buttonText.fr = translations.fr.buttonText;
|
|
92
|
+
|
|
93
|
+
// Support other languages
|
|
94
|
+
Object.keys(translations).forEach((lang) => {
|
|
95
|
+
if (lang !== 'en' && lang !== 'fr' && translations[lang]?.buttonText) {
|
|
96
|
+
buttonText[lang] = translations[lang]!.buttonText!;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
buttonText,
|
|
102
|
+
themeColor: '#2DBD60', // Default theme color
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Transform welcome config
|
|
108
|
+
*/
|
|
109
|
+
function transformWelcomeConfig(config: any): WelcomeConfig {
|
|
110
|
+
return {
|
|
111
|
+
subtitle: config.subtitle,
|
|
112
|
+
buttonText: config.buttonText,
|
|
113
|
+
requirements: config.requirements || [],
|
|
114
|
+
estimatedTime: config.estimatedTime,
|
|
115
|
+
consentOptions: config.consentOptions || {
|
|
116
|
+
showPrivacyPolicy: true,
|
|
117
|
+
showTermsOfService: true,
|
|
118
|
+
showMarketingConsent: false,
|
|
119
|
+
},
|
|
120
|
+
welcomeMessage: config.welcomeMessage,
|
|
121
|
+
showEstimatedTime: config.showEstimatedTime !== false,
|
|
122
|
+
} as WelcomeConfig;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Transform government ID config to IDCardConfig
|
|
127
|
+
*/
|
|
128
|
+
function transformGovernmentIdConfig(config: any): IDCardConfig {
|
|
129
|
+
const documentTypes: string[] = config.documentTypes || ['passport', 'nationalId'];
|
|
130
|
+
const sides: string[] = [];
|
|
131
|
+
|
|
132
|
+
if (config.requiredSides === 'front-back' || config.requiredSides === 'both') {
|
|
133
|
+
sides.push('front', 'back');
|
|
134
|
+
} else if (config.requiredSides === 'front') {
|
|
135
|
+
sides.push('front');
|
|
136
|
+
} else if (config.requiredSides === 'back') {
|
|
137
|
+
sides.push('back');
|
|
138
|
+
} else {
|
|
139
|
+
// Default to both sides
|
|
140
|
+
sides.push('front', 'back');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const idCardConfig: IDCardConfig = {
|
|
144
|
+
sides,
|
|
145
|
+
allowed_formats: ['jpg', 'jpeg', 'png'],
|
|
146
|
+
max_size_mb: 10,
|
|
147
|
+
document_types: documentTypes.map((dt: string) => {
|
|
148
|
+
const mapping: Record<string, any> = {
|
|
149
|
+
'passport': 'passport',
|
|
150
|
+
'nationalId': 'national_id',
|
|
151
|
+
'driversLicense': 'drivers_licence',
|
|
152
|
+
'identityCard': 'identity_card',
|
|
153
|
+
};
|
|
154
|
+
return mapping[dt] || dt;
|
|
155
|
+
}) as any[],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Add bbox configs if available
|
|
159
|
+
if (config.cameraOverlay?.bbox) {
|
|
160
|
+
const bbox = config.cameraOverlay.bbox;
|
|
161
|
+
idCardConfig.bbox_configs = {} as Record<GovernmentDocumentType, {
|
|
162
|
+
xMin: number;
|
|
163
|
+
yMin: number;
|
|
164
|
+
xMax: number;
|
|
165
|
+
yMax: number;
|
|
166
|
+
borderColor?: string;
|
|
167
|
+
borderWidth?: number;
|
|
168
|
+
cornerRadius?: number;
|
|
169
|
+
}>;
|
|
170
|
+
documentTypes.forEach((dt: string) => {
|
|
171
|
+
const docType = dt as GovernmentDocumentType;
|
|
172
|
+
(idCardConfig.bbox_configs as any)[docType] = {
|
|
173
|
+
xMin: bbox.xMin || 20,
|
|
174
|
+
yMin: bbox.yMin || 140,
|
|
175
|
+
xMax: bbox.xMax || 370,
|
|
176
|
+
yMax: bbox.yMax || 340,
|
|
177
|
+
borderColor: bbox.borderColor || '#2DBD60',
|
|
178
|
+
borderWidth: bbox.borderWidth || 3,
|
|
179
|
+
cornerRadius: bbox.cornerRadius || 8,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Store additional backend-specific config in a way that can be accessed
|
|
185
|
+
(idCardConfig as any).authenticationMethods = config.authenticationMethods;
|
|
186
|
+
(idCardConfig as any).documentTypesByCountry = config.documentTypesByCountry;
|
|
187
|
+
(idCardConfig as any).selectedCountries = config.selectedCountries;
|
|
188
|
+
(idCardConfig as any).instructionsByDocumentType = config.instructionsByDocumentType;
|
|
189
|
+
(idCardConfig as any).cameraSettings = config.cameraSettings;
|
|
190
|
+
(idCardConfig as any).validationRules = config.validationRules;
|
|
191
|
+
|
|
192
|
+
return idCardConfig;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Transform location capture config
|
|
197
|
+
*/
|
|
198
|
+
function transformLocationConfig(config: any): LocationConfig {
|
|
199
|
+
return {
|
|
200
|
+
accuracy: config.accuracy || 'high',
|
|
201
|
+
required: config.required !== false,
|
|
202
|
+
} as LocationConfig;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Transform selfie capture config
|
|
207
|
+
*/
|
|
208
|
+
function transformSelfieConfig(config: any): SelfieConfig {
|
|
209
|
+
return {
|
|
210
|
+
liveness_check: config.livenessEnabled !== false,
|
|
211
|
+
max_attempts: config.maxAttempts || 5,
|
|
212
|
+
orientations: config.orientations || ['center'],
|
|
213
|
+
} as SelfieConfig;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Transform component config based on type
|
|
218
|
+
*/
|
|
219
|
+
function transformComponentConfig(
|
|
220
|
+
type: TemplateComponent['type'],
|
|
221
|
+
backendConfig: any
|
|
222
|
+
): ComponentConfig {
|
|
223
|
+
switch (type) {
|
|
224
|
+
case 'welcome':
|
|
225
|
+
return transformWelcomeConfig(backendConfig);
|
|
226
|
+
case 'id_card':
|
|
227
|
+
return transformGovernmentIdConfig(backendConfig);
|
|
228
|
+
case 'location':
|
|
229
|
+
return transformLocationConfig(backendConfig);
|
|
230
|
+
case 'selfie':
|
|
231
|
+
return transformSelfieConfig(backendConfig);
|
|
232
|
+
case 'file_upload':
|
|
233
|
+
return {
|
|
234
|
+
allowed_formats: backendConfig.allowed_formats || ['jpg', 'jpeg', 'png', 'pdf'],
|
|
235
|
+
max_size_mb: backendConfig.max_size_mb || 10,
|
|
236
|
+
required: backendConfig.required !== false,
|
|
237
|
+
} as FileUploadConfig;
|
|
238
|
+
case 'country_selection':
|
|
239
|
+
return {
|
|
240
|
+
allowed_countries: backendConfig.allowed_countries || [],
|
|
241
|
+
default_country: backendConfig.default_country || '',
|
|
242
|
+
required: backendConfig.required !== false,
|
|
243
|
+
} as CountrySelectionConfig;
|
|
244
|
+
default:
|
|
245
|
+
return backendConfig as ComponentConfig;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Transform a single backend component to SDK component
|
|
251
|
+
*/
|
|
252
|
+
function transformComponent(
|
|
253
|
+
backendComponent: BackendTemplateComponent,
|
|
254
|
+
componentIndex: number
|
|
255
|
+
): TemplateComponent {
|
|
256
|
+
const mappedType = COMPONENT_TYPE_MAPPING[backendComponent.type];
|
|
257
|
+
|
|
258
|
+
if (!mappedType) {
|
|
259
|
+
logger.warn(`Unknown component type: ${backendComponent.type}, defaulting to initialization`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const componentId = hashStringToNumber(backendComponent.id);
|
|
263
|
+
const { labels, instructions } = transformTranslations(backendComponent.translations);
|
|
264
|
+
const ui = transformUI(backendComponent.translations);
|
|
265
|
+
const config = transformComponentConfig(mappedType || 'initialization', backendComponent.config);
|
|
266
|
+
|
|
267
|
+
// For components with multiple sides (like id_card), create nested structure
|
|
268
|
+
let finalLabels: LocalizedText | Record<string, LocalizedText> = labels;
|
|
269
|
+
let finalInstructions: LocalizedText | Record<string, LocalizedText> = instructions;
|
|
270
|
+
let finalUI: ComponentUI | Record<string, ComponentUI> = ui;
|
|
271
|
+
|
|
272
|
+
if (mappedType === 'id_card' && (backendComponent.config as GovernmentIdConfig).requiredSides === 'front-back') {
|
|
273
|
+
// Create separate labels/instructions for front and back
|
|
274
|
+
finalLabels = {
|
|
275
|
+
front: {
|
|
276
|
+
en: backendComponent.translations.en?.title || labels.en,
|
|
277
|
+
fr: backendComponent.translations.fr?.title || labels.fr,
|
|
278
|
+
},
|
|
279
|
+
back: {
|
|
280
|
+
en: `${labels.en} (Back)`,
|
|
281
|
+
fr: `${labels.fr} (Verso)`,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
finalInstructions = {
|
|
285
|
+
front: instructions,
|
|
286
|
+
back: instructions,
|
|
287
|
+
};
|
|
288
|
+
finalUI = {
|
|
289
|
+
front: ui,
|
|
290
|
+
back: ui,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
id: componentId,
|
|
296
|
+
type: mappedType || 'initialization',
|
|
297
|
+
order: backendComponent.order,
|
|
298
|
+
labels: finalLabels,
|
|
299
|
+
instructions: finalInstructions,
|
|
300
|
+
ui: finalUI,
|
|
301
|
+
config,
|
|
302
|
+
required: backendComponent.required !== false,
|
|
303
|
+
} as TemplateComponent;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Transform backend template to SDK template format
|
|
308
|
+
*/
|
|
309
|
+
export function transformBackendTemplateToSDK(
|
|
310
|
+
backendTemplate: BackendKYCTemplate
|
|
311
|
+
): KYCTemplate {
|
|
312
|
+
try {
|
|
313
|
+
// Validate backend template
|
|
314
|
+
if (!backendTemplate.id || !backendTemplate.name || !Array.isArray(backendTemplate.components)) {
|
|
315
|
+
throw new Error('Invalid backend template structure');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Sort components by order
|
|
319
|
+
const sortedComponents = [...backendTemplate.components].sort(
|
|
320
|
+
(a, b) => a.order - b.order
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// Transform each component
|
|
324
|
+
const transformedComponents = sortedComponents.map((comp, index) =>
|
|
325
|
+
transformComponent(comp, index)
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// Auto-create country_selection component before id_card if government-id has selection data
|
|
329
|
+
// Check if country_selection already exists in backend template
|
|
330
|
+
const hasCountrySelectionInBackend = sortedComponents.some(c => (c as any).type === 'country-selection');
|
|
331
|
+
|
|
332
|
+
if (!hasCountrySelectionInBackend) {
|
|
333
|
+
// Find the first government-id component with selection data
|
|
334
|
+
const govIdWithSelection = sortedComponents.find(backendComp =>
|
|
335
|
+
backendComp.type === 'government-id' &&
|
|
336
|
+
backendComp.config && (
|
|
337
|
+
(backendComp.config as any).selectedCountries ||
|
|
338
|
+
(backendComp.config as any).documentTypesByCountry
|
|
339
|
+
)
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
if (govIdWithSelection) {
|
|
343
|
+
// Find the corresponding transformed id_card component
|
|
344
|
+
const transformedIdCard = transformedComponents.find(c =>
|
|
345
|
+
hashStringToNumber(govIdWithSelection.id) === c.id
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
if (transformedIdCard) {
|
|
349
|
+
// Create a country_selection component before id_card
|
|
350
|
+
const countrySelectionComponent: TemplateComponent = {
|
|
351
|
+
id: hashStringToNumber(`${govIdWithSelection.id}-country-selection`),
|
|
352
|
+
type: 'country_selection',
|
|
353
|
+
order: transformedIdCard.order - 0.5, // Insert before id_card
|
|
354
|
+
labels: {
|
|
355
|
+
en: govIdWithSelection.translations.en?.title || 'Select Country and Document',
|
|
356
|
+
fr: govIdWithSelection.translations.fr?.title || 'Sélectionnez le pays et le document',
|
|
357
|
+
},
|
|
358
|
+
instructions: {
|
|
359
|
+
en: 'Please select your country and document type',
|
|
360
|
+
fr: 'Veuillez sélectionner votre pays et le type de document',
|
|
361
|
+
},
|
|
362
|
+
ui: {
|
|
363
|
+
themeColor: '#2DBD60',
|
|
364
|
+
buttonText: {
|
|
365
|
+
en: 'Continue',
|
|
366
|
+
fr: 'Continuer',
|
|
367
|
+
},
|
|
368
|
+
} as ComponentUI,
|
|
369
|
+
config: {
|
|
370
|
+
allowed_countries: (govIdWithSelection.config as any).selectedCountries || [],
|
|
371
|
+
default_country: '',
|
|
372
|
+
required: true,
|
|
373
|
+
} as CountrySelectionConfig,
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// Insert country_selection before id_card in transformedComponents
|
|
377
|
+
const idCardIndex = transformedComponents.indexOf(transformedIdCard);
|
|
378
|
+
transformedComponents.splice(idCardIndex, 0, countrySelectionComponent);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Re-assign order values sequentially after insertion
|
|
384
|
+
transformedComponents.forEach((comp, index) => {
|
|
385
|
+
comp.order = index + 1;
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Create SDK template
|
|
389
|
+
const sdkTemplate: KYCTemplate = {
|
|
390
|
+
id: hashStringToNumber(backendTemplate.id),
|
|
391
|
+
name: backendTemplate.name as LocalizedText,
|
|
392
|
+
description: backendTemplate.description || {
|
|
393
|
+
en: '',
|
|
394
|
+
fr: '',
|
|
395
|
+
},
|
|
396
|
+
version: backendTemplate.version || '1.0.0',
|
|
397
|
+
components: transformedComponents,
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
logger.log(`Template transformed: ${backendTemplate.id} -> ${sdkTemplate.id} with ${transformedComponents.length} components`);
|
|
401
|
+
|
|
402
|
+
return sdkTemplate;
|
|
403
|
+
} catch (error: any) {
|
|
404
|
+
logger.error('Error transforming template:', error);
|
|
405
|
+
throw new Error(`Failed to transform template: ${error.message}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Validate transformed template
|
|
411
|
+
*/
|
|
412
|
+
export function validateTransformedTemplate(template: KYCTemplate): boolean {
|
|
413
|
+
if (!template.id || !template.name || !Array.isArray(template.components)) {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (!template.name.en || !template.name.fr) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Validate each component
|
|
422
|
+
for (const component of template.components) {
|
|
423
|
+
if (!component.id || !component.type || typeof component.order !== 'number') {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!component.labels || !component.instructions || !component.config) {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return true;
|
|
433
|
+
}
|
package/src/web/WebKYCEntry.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React, { useEffect, useState, useCallback } from 'react';
|
|
2
|
-
import { View, Text, StyleSheet, SafeAreaView } from 'react-native';
|
|
2
|
+
import { View, Text, StyleSheet, SafeAreaView, ActivityIndicator } from 'react-native';
|
|
3
3
|
// import { TemplateKYCFlow } from '../components/TemplateKYCFlowRefactored';
|
|
4
4
|
// import { KYCTemplate } from '../types/KYC.types';
|
|
5
5
|
import { useI18n } from '../hooks/useI18n';
|
|
6
6
|
import { TemplateKYCExample } from '../components/TemplateKYCExample';
|
|
7
|
+
import { isCallbackUrlAllowed, generateCallbackSignature } from '../config/allowedDomains';
|
|
7
8
|
|
|
8
9
|
interface WebKYCEntryProps {
|
|
9
10
|
onComplete?: (data: any) => void;
|
|
@@ -17,6 +18,13 @@ interface URLParams {
|
|
|
17
18
|
lang?: string;
|
|
18
19
|
theme?: string;
|
|
19
20
|
kyc_id?: string;
|
|
21
|
+
secret?: string; // Optional secret for signature generation
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface VerificationSteps {
|
|
25
|
+
document_analyzed?: boolean;
|
|
26
|
+
selfie_analyzed?: boolean;
|
|
27
|
+
liveness_checked?: boolean;
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
const WebKYCEntry: React.FC<WebKYCEntryProps> = ({
|
|
@@ -28,6 +36,7 @@ const WebKYCEntry: React.FC<WebKYCEntryProps> = ({
|
|
|
28
36
|
const [urlParams, setUrlParams] = useState<URLParams>({});
|
|
29
37
|
const [isLoading, setIsLoading] = useState(true);
|
|
30
38
|
const [error, setError] = useState<string | null>(null);
|
|
39
|
+
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
|
31
40
|
|
|
32
41
|
// Parse URL parameters
|
|
33
42
|
const parseUrlParams = useCallback((): URLParams => {
|
|
@@ -40,75 +49,115 @@ const WebKYCEntry: React.FC<WebKYCEntryProps> = ({
|
|
|
40
49
|
lang: urlParams.get('lang') || 'en',
|
|
41
50
|
theme: urlParams.get('theme') || 'light',
|
|
42
51
|
kyc_id: urlParams.get('kyc_id') || undefined,
|
|
52
|
+
secret: urlParams.get('secret') || undefined,
|
|
43
53
|
};
|
|
44
54
|
}, []);
|
|
45
55
|
|
|
46
|
-
// Safe redirect function with validation
|
|
47
|
-
const redirectToReturnUrl = useCallback((params: {
|
|
56
|
+
// Safe redirect function with enhanced validation
|
|
57
|
+
const redirectToReturnUrl = useCallback(async (params: {
|
|
48
58
|
status: 'completed' | 'cancelled' | 'error';
|
|
49
59
|
kyc_id?: string;
|
|
50
60
|
message?: string;
|
|
51
|
-
|
|
61
|
+
processing_state?: 'analyzing' | 'completed' | 'pending';
|
|
62
|
+
verification_steps?: VerificationSteps;
|
|
52
63
|
}) => {
|
|
53
|
-
const { return_url } = urlParams;
|
|
64
|
+
const { return_url, secret } = urlParams;
|
|
54
65
|
|
|
55
66
|
if (!return_url) {
|
|
56
67
|
console.warn('No return_url provided');
|
|
57
68
|
return;
|
|
58
69
|
}
|
|
59
70
|
|
|
60
|
-
// Basic URL validation - ensure it's a valid URL
|
|
61
71
|
try {
|
|
72
|
+
// Enhanced URL validation with domain whitelist
|
|
73
|
+
const validation = isCallbackUrlAllowed(return_url);
|
|
74
|
+
if (!validation.allowed) {
|
|
75
|
+
console.error('Callback URL validation failed:', validation.reason);
|
|
76
|
+
setError(`Security Error: ${validation.reason}`);
|
|
77
|
+
|
|
78
|
+
// Log suspicious redirect attempt
|
|
79
|
+
console.warn('Suspicious redirect attempt blocked:', {
|
|
80
|
+
url: return_url,
|
|
81
|
+
reason: validation.reason,
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
62
87
|
const returnUrl = new URL(return_url);
|
|
63
|
-
|
|
64
|
-
// Optional: Add domain allowlist validation here
|
|
65
|
-
// const allowedDomains = ['example.com', 'trusted-site.com'];
|
|
66
|
-
// if (!allowedDomains.some(domain => returnUrl.hostname.endsWith(domain))) {
|
|
67
|
-
// console.error('Return URL not in allowlist');
|
|
68
|
-
// return;
|
|
69
|
-
// }
|
|
70
88
|
|
|
71
89
|
// Build redirect URL with parameters
|
|
72
90
|
const redirectUrl = new URL(returnUrl);
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
const redirectParams: Record<string, string> = {
|
|
92
|
+
status: params.status,
|
|
93
|
+
};
|
|
94
|
+
|
|
75
95
|
if (params.kyc_id) {
|
|
76
|
-
|
|
96
|
+
redirectParams.kyc_id = params.kyc_id;
|
|
77
97
|
}
|
|
78
98
|
|
|
79
99
|
if (params.message) {
|
|
80
|
-
|
|
100
|
+
redirectParams.message = params.message;
|
|
81
101
|
}
|
|
82
|
-
|
|
83
|
-
if (params.
|
|
84
|
-
|
|
102
|
+
|
|
103
|
+
if (params.processing_state) {
|
|
104
|
+
redirectParams.processing_state = params.processing_state;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (params.verification_steps) {
|
|
108
|
+
redirectParams.verification_steps = JSON.stringify(params.verification_steps);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Generate signature for integrity verification
|
|
112
|
+
if (secret) {
|
|
113
|
+
const signature = await generateCallbackSignature(redirectParams, secret);
|
|
114
|
+
if (signature) {
|
|
115
|
+
redirectParams.sig = signature;
|
|
116
|
+
}
|
|
85
117
|
}
|
|
86
118
|
|
|
87
|
-
//
|
|
119
|
+
// Add all params to URL
|
|
120
|
+
Object.entries(redirectParams).forEach(([key, value]) => {
|
|
121
|
+
redirectUrl.searchParams.set(key, value);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Send postMessage to parent if in iframe
|
|
88
125
|
if (window.parent !== window) {
|
|
89
126
|
window.parent.postMessage({
|
|
90
127
|
type: 'kyc_result',
|
|
91
128
|
status: params.status,
|
|
92
129
|
kyc_id: params.kyc_id,
|
|
93
130
|
message: params.message,
|
|
94
|
-
|
|
131
|
+
processing_state: params.processing_state,
|
|
132
|
+
verification_steps: params.verification_steps,
|
|
133
|
+
}, returnUrl.origin); // Use specific origin instead of '*'
|
|
95
134
|
}
|
|
96
135
|
|
|
97
136
|
// Redirect to return URL
|
|
98
137
|
window.location.href = redirectUrl.toString();
|
|
99
138
|
} catch (error) {
|
|
100
|
-
console.error('
|
|
101
|
-
setError('
|
|
139
|
+
console.error('Error during redirect:', error);
|
|
140
|
+
setError('Failed to redirect to callback URL');
|
|
102
141
|
}
|
|
103
142
|
}, [urlParams]);
|
|
104
143
|
|
|
105
144
|
// Handle KYC completion
|
|
106
145
|
const handleComplete = useCallback((data: any) => {
|
|
107
146
|
console.log('KYC completed:', data);
|
|
147
|
+
|
|
148
|
+
// Check if still processing/analyzing
|
|
149
|
+
const isStillProcessing = data.isProcessing || data.session?.isProcessing;
|
|
150
|
+
|
|
108
151
|
redirectToReturnUrl({
|
|
109
152
|
status: 'completed',
|
|
110
153
|
kyc_id: data.session_id || urlParams.kyc_id,
|
|
111
154
|
message: 'KYC process completed successfully',
|
|
155
|
+
processing_state: isStillProcessing ? 'analyzing' : 'completed',
|
|
156
|
+
verification_steps: {
|
|
157
|
+
document_analyzed: data.documentAnalysisComplete || false,
|
|
158
|
+
selfie_analyzed: data.selfieAnalysisComplete || false,
|
|
159
|
+
liveness_checked: data.livenessCheckComplete || false,
|
|
160
|
+
},
|
|
112
161
|
});
|
|
113
162
|
onComplete?.(data);
|
|
114
163
|
}, [redirectToReturnUrl, urlParams.kyc_id, onComplete]);
|
|
@@ -116,10 +165,12 @@ const WebKYCEntry: React.FC<WebKYCEntryProps> = ({
|
|
|
116
165
|
// Handle KYC error
|
|
117
166
|
const handleError = useCallback((error: string) => {
|
|
118
167
|
console.error('KYC error:', error);
|
|
168
|
+
setIsAnalyzing(false);
|
|
119
169
|
redirectToReturnUrl({
|
|
120
170
|
status: 'error',
|
|
121
171
|
kyc_id: urlParams.kyc_id,
|
|
122
172
|
message: error,
|
|
173
|
+
processing_state: 'pending',
|
|
123
174
|
});
|
|
124
175
|
onError?.(error);
|
|
125
176
|
}, [redirectToReturnUrl, urlParams.kyc_id, onError]);
|
|
@@ -127,10 +178,12 @@ const WebKYCEntry: React.FC<WebKYCEntryProps> = ({
|
|
|
127
178
|
// Handle KYC cancellation
|
|
128
179
|
const handleCancel = useCallback(() => {
|
|
129
180
|
console.log('KYC cancelled');
|
|
181
|
+
setIsAnalyzing(false);
|
|
130
182
|
redirectToReturnUrl({
|
|
131
183
|
status: 'cancelled',
|
|
132
184
|
kyc_id: urlParams.kyc_id,
|
|
133
185
|
message: 'KYC process was cancelled',
|
|
186
|
+
processing_state: 'pending',
|
|
134
187
|
});
|
|
135
188
|
onCancel?.();
|
|
136
189
|
}, [redirectToReturnUrl, urlParams.kyc_id, onCancel]);
|
|
@@ -181,6 +234,15 @@ const WebKYCEntry: React.FC<WebKYCEntryProps> = ({
|
|
|
181
234
|
|
|
182
235
|
return (
|
|
183
236
|
<SafeAreaView style={styles.container}>
|
|
237
|
+
{isAnalyzing && (
|
|
238
|
+
<View style={styles.analyzingOverlay}>
|
|
239
|
+
<View style={styles.analyzingContainer}>
|
|
240
|
+
<ActivityIndicator size="large" color="#2DBD60" />
|
|
241
|
+
<Text style={styles.analyzingText}>Analyzing verification data...</Text>
|
|
242
|
+
<Text style={styles.analyzingSubtext}>This may take a few moments</Text>
|
|
243
|
+
</View>
|
|
244
|
+
</View>
|
|
245
|
+
)}
|
|
184
246
|
<TemplateKYCExample
|
|
185
247
|
onComplete={handleComplete}
|
|
186
248
|
onCancel={handleCancel}
|
|
@@ -196,6 +258,7 @@ const styles = StyleSheet.create({
|
|
|
196
258
|
container: {
|
|
197
259
|
flex: 1,
|
|
198
260
|
backgroundColor: '#f5f5f5',
|
|
261
|
+
overflow: 'visible' as any, // Allow scrolling on web
|
|
199
262
|
},
|
|
200
263
|
loadingText: {
|
|
201
264
|
fontSize: 18,
|
|
@@ -210,6 +273,41 @@ const styles = StyleSheet.create({
|
|
|
210
273
|
color: '#dc2626',
|
|
211
274
|
paddingHorizontal: 20,
|
|
212
275
|
},
|
|
276
|
+
analyzingOverlay: {
|
|
277
|
+
position: 'absolute',
|
|
278
|
+
top: 0,
|
|
279
|
+
left: 0,
|
|
280
|
+
right: 0,
|
|
281
|
+
bottom: 0,
|
|
282
|
+
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
|
283
|
+
justifyContent: 'center',
|
|
284
|
+
alignItems: 'center',
|
|
285
|
+
zIndex: 9999,
|
|
286
|
+
},
|
|
287
|
+
analyzingContainer: {
|
|
288
|
+
backgroundColor: 'white',
|
|
289
|
+
borderRadius: 12,
|
|
290
|
+
padding: 32,
|
|
291
|
+
alignItems: 'center',
|
|
292
|
+
shadowColor: '#000',
|
|
293
|
+
shadowOffset: { width: 0, height: 4 },
|
|
294
|
+
shadowOpacity: 0.3,
|
|
295
|
+
shadowRadius: 8,
|
|
296
|
+
elevation: 8,
|
|
297
|
+
},
|
|
298
|
+
analyzingText: {
|
|
299
|
+
fontSize: 18,
|
|
300
|
+
fontWeight: '600',
|
|
301
|
+
color: '#333',
|
|
302
|
+
marginTop: 16,
|
|
303
|
+
textAlign: 'center',
|
|
304
|
+
},
|
|
305
|
+
analyzingSubtext: {
|
|
306
|
+
fontSize: 14,
|
|
307
|
+
color: '#666',
|
|
308
|
+
marginTop: 8,
|
|
309
|
+
textAlign: 'center',
|
|
310
|
+
},
|
|
213
311
|
});
|
|
214
312
|
|
|
215
313
|
export default WebKYCEntry;
|