@transfergratis/react-native-sdk 0.1.23 → 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.
Files changed (137) hide show
  1. package/android/src/main/AndroidManifest.xml +9 -4
  2. package/build/components/EnhancedCameraView.d.ts.map +1 -1
  3. package/build/components/EnhancedCameraView.js +26 -3
  4. package/build/components/EnhancedCameraView.js.map +1 -1
  5. package/build/components/EnhancedCameraView.web.d.ts.map +1 -1
  6. package/build/components/EnhancedCameraView.web.js +21 -0
  7. package/build/components/EnhancedCameraView.web.js.map +1 -1
  8. package/build/components/KYCElements/CameraCapture.d.ts.map +1 -1
  9. package/build/components/KYCElements/CameraCapture.js +4 -3
  10. package/build/components/KYCElements/CameraCapture.js.map +1 -1
  11. package/build/components/KYCElements/CountrySelectionTemplate.d.ts +5 -2
  12. package/build/components/KYCElements/CountrySelectionTemplate.d.ts.map +1 -1
  13. package/build/components/KYCElements/CountrySelectionTemplate.js +360 -101
  14. package/build/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
  15. package/build/components/KYCElements/FileUpload.d.ts.map +1 -1
  16. package/build/components/KYCElements/FileUpload.js +5 -4
  17. package/build/components/KYCElements/FileUpload.js.map +1 -1
  18. package/build/components/KYCElements/FileUploadTemplate.d.ts.map +1 -1
  19. package/build/components/KYCElements/FileUploadTemplate.js +5 -4
  20. package/build/components/KYCElements/FileUploadTemplate.js.map +1 -1
  21. package/build/components/KYCElements/IDCardCapture.d.ts.map +1 -1
  22. package/build/components/KYCElements/IDCardCapture.js +193 -237
  23. package/build/components/KYCElements/IDCardCapture.js.map +1 -1
  24. package/build/components/KYCElements/LocationCaptureTemplate.d.ts.map +1 -1
  25. package/build/components/KYCElements/LocationCaptureTemplate.js +78 -37
  26. package/build/components/KYCElements/LocationCaptureTemplate.js.map +1 -1
  27. package/build/components/KYCElements/OrientationVideoCapture.js +3 -2
  28. package/build/components/KYCElements/OrientationVideoCapture.js.map +1 -1
  29. package/build/components/KYCElements/OrientationVideoCaptureEnhanced.js +3 -2
  30. package/build/components/KYCElements/OrientationVideoCaptureEnhanced.js.map +1 -1
  31. package/build/components/KYCElements/OrientationVideoCaptureFinal.js +3 -2
  32. package/build/components/KYCElements/OrientationVideoCaptureFinal.js.map +1 -1
  33. package/build/components/KYCElements/SelfieCapture.d.ts.map +1 -1
  34. package/build/components/KYCElements/SelfieCapture.js +4 -3
  35. package/build/components/KYCElements/SelfieCapture.js.map +1 -1
  36. package/build/components/KYCElements/SelfieCaptureTemplate.d.ts.map +1 -1
  37. package/build/components/KYCElements/SelfieCaptureTemplate.js +182 -39
  38. package/build/components/KYCElements/SelfieCaptureTemplate.js.map +1 -1
  39. package/build/components/KYCElements/WelcomeTemplate.d.ts +12 -0
  40. package/build/components/KYCElements/WelcomeTemplate.d.ts.map +1 -0
  41. package/build/components/KYCElements/WelcomeTemplate.js +243 -0
  42. package/build/components/KYCElements/WelcomeTemplate.js.map +1 -0
  43. package/build/components/TemplateKYCExample.d.ts +4 -2
  44. package/build/components/TemplateKYCExample.d.ts.map +1 -1
  45. package/build/components/TemplateKYCExample.js +5 -69
  46. package/build/components/TemplateKYCExample.js.map +1 -1
  47. package/build/components/TemplateKYCFlowRefactored.d.ts +2 -1
  48. package/build/components/TemplateKYCFlowRefactored.d.ts.map +1 -1
  49. package/build/components/TemplateKYCFlowRefactored.js +95 -10
  50. package/build/components/TemplateKYCFlowRefactored.js.map +1 -1
  51. package/build/components/example/DynamicTemplateExample.d.ts +10 -0
  52. package/build/components/example/DynamicTemplateExample.d.ts.map +1 -0
  53. package/build/components/example/DynamicTemplateExample.js +241 -0
  54. package/build/components/example/DynamicTemplateExample.js.map +1 -0
  55. package/build/config/allowedDomains.d.ts +30 -0
  56. package/build/config/allowedDomains.d.ts.map +1 -0
  57. package/build/config/allowedDomains.js +127 -0
  58. package/build/config/allowedDomains.js.map +1 -0
  59. package/build/hooks/useTemplateKYCFlow.d.ts.map +1 -1
  60. package/build/hooks/useTemplateKYCFlow.js +31 -11
  61. package/build/hooks/useTemplateKYCFlow.js.map +1 -1
  62. package/build/hooks/useTemplateLoader.d.ts +14 -0
  63. package/build/hooks/useTemplateLoader.d.ts.map +1 -0
  64. package/build/hooks/useTemplateLoader.js +85 -0
  65. package/build/hooks/useTemplateLoader.js.map +1 -0
  66. package/build/i18n/en/index.d.ts +9 -0
  67. package/build/i18n/en/index.d.ts.map +1 -1
  68. package/build/i18n/en/index.js +9 -0
  69. package/build/i18n/en/index.js.map +1 -1
  70. package/build/i18n/fr/index.d.ts +9 -0
  71. package/build/i18n/fr/index.d.ts.map +1 -1
  72. package/build/i18n/fr/index.js +9 -0
  73. package/build/i18n/fr/index.js.map +1 -1
  74. package/build/index.d.ts +5 -0
  75. package/build/index.d.ts.map +1 -1
  76. package/build/index.js +8 -0
  77. package/build/index.js.map +1 -1
  78. package/build/modules/api/CardAuthentification.js +1 -0
  79. package/build/modules/api/CardAuthentification.js.map +1 -1
  80. package/build/modules/api/KYCService.d.ts.map +1 -1
  81. package/build/modules/api/KYCService.js +7 -2
  82. package/build/modules/api/KYCService.js.map +1 -1
  83. package/build/modules/api/TemplateService.d.ts +45 -0
  84. package/build/modules/api/TemplateService.d.ts.map +1 -0
  85. package/build/modules/api/TemplateService.js +145 -0
  86. package/build/modules/api/TemplateService.js.map +1 -0
  87. package/build/types/KYC.types.d.ts +144 -4
  88. package/build/types/KYC.types.d.ts.map +1 -1
  89. package/build/types/KYC.types.js +15 -0
  90. package/build/types/KYC.types.js.map +1 -1
  91. package/build/utils/cropByObb.d.ts +1 -0
  92. package/build/utils/cropByObb.d.ts.map +1 -1
  93. package/build/utils/cropByObb.js +70 -0
  94. package/build/utils/cropByObb.js.map +1 -1
  95. package/build/utils/platformAlert.d.ts +20 -0
  96. package/build/utils/platformAlert.d.ts.map +1 -0
  97. package/build/utils/platformAlert.js +67 -0
  98. package/build/utils/platformAlert.js.map +1 -0
  99. package/build/utils/template-transformer.d.ts +10 -0
  100. package/build/utils/template-transformer.d.ts.map +1 -0
  101. package/build/utils/template-transformer.js +353 -0
  102. package/build/utils/template-transformer.js.map +1 -0
  103. package/build/web/WebKYCEntry.d.ts.map +1 -1
  104. package/build/web/WebKYCEntry.js +102 -20
  105. package/build/web/WebKYCEntry.js.map +1 -1
  106. package/package.json +1 -1
  107. package/src/components/EnhancedCameraView.tsx +31 -2
  108. package/src/components/EnhancedCameraView.web.tsx +24 -0
  109. package/src/components/KYCElements/CameraCapture.tsx +4 -3
  110. package/src/components/KYCElements/CountrySelectionTemplate.tsx +410 -113
  111. package/src/components/KYCElements/FileUpload.tsx +5 -4
  112. package/src/components/KYCElements/FileUploadTemplate.tsx +5 -4
  113. package/src/components/KYCElements/IDCardCapture.tsx +196 -254
  114. package/src/components/KYCElements/LocationCaptureTemplate.tsx +95 -44
  115. package/src/components/KYCElements/OrientationVideoCapture.tsx +2 -2
  116. package/src/components/KYCElements/OrientationVideoCaptureEnhanced.tsx +2 -2
  117. package/src/components/KYCElements/OrientationVideoCaptureFinal.tsx +2 -2
  118. package/src/components/KYCElements/SelfieCapture.tsx +4 -3
  119. package/src/components/KYCElements/SelfieCaptureTemplate.tsx +195 -41
  120. package/src/components/KYCElements/WelcomeTemplate.tsx +289 -0
  121. package/src/components/TemplateKYCExample.tsx +16 -72
  122. package/src/components/TemplateKYCFlowRefactored.tsx +122 -12
  123. package/src/components/example/DynamicTemplateExample.tsx +289 -0
  124. package/src/config/allowedDomains.ts +152 -0
  125. package/src/hooks/useTemplateKYCFlow.tsx +33 -11
  126. package/src/hooks/useTemplateLoader.ts +102 -0
  127. package/src/i18n/en/index.ts +10 -0
  128. package/src/i18n/fr/index.ts +9 -0
  129. package/src/index.ts +11 -0
  130. package/src/modules/api/CardAuthentification.ts +1 -1
  131. package/src/modules/api/KYCService.ts +12 -8
  132. package/src/modules/api/TemplateService.ts +167 -0
  133. package/src/types/KYC.types.ts +188 -3
  134. package/src/utils/cropByObb.ts +83 -3
  135. package/src/utils/platformAlert.ts +85 -0
  136. package/src/utils/template-transformer.ts +433 -0
  137. 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
+ }
@@ -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
- sig?: string;
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
- redirectUrl.searchParams.set('status', params.status);
74
-
91
+ const redirectParams: Record<string, string> = {
92
+ status: params.status,
93
+ };
94
+
75
95
  if (params.kyc_id) {
76
- redirectUrl.searchParams.set('kyc_id', params.kyc_id);
96
+ redirectParams.kyc_id = params.kyc_id;
77
97
  }
78
98
 
79
99
  if (params.message) {
80
- redirectUrl.searchParams.set('message', params.message);
100
+ redirectParams.message = params.message;
81
101
  }
82
-
83
- if (params.sig) {
84
- redirectUrl.searchParams.set('sig', params.sig);
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
- // Optional: Send postMessage to parent if in iframe
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('Invalid return URL:', error);
101
- setError('Invalid return URL provided');
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;