@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.
Files changed (182) hide show
  1. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml +12 -5
  2. package/android/build/intermediates/aar_main_jar/debug/syncDebugLibJars/classes.jar +0 -0
  3. package/android/build/intermediates/annotations_typedef_file/debug/extractDebugAnnotations/typedefs.txt +0 -0
  4. package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +1 -1
  5. package/android/build/intermediates/incremental/debug-mergeJavaRes/merge-state +0 -0
  6. package/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt +61 -59
  7. package/android/build/intermediates/merged_java_res/debug/mergeDebugJavaResource/feature-transfergratis-react-native-sdk.jar +0 -0
  8. package/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml +12 -5
  9. package/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin +0 -0
  10. package/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin +0 -0
  11. package/android/build/outputs/aar/transfergratis-react-native-sdk-debug.aar +0 -0
  12. package/android/build/outputs/logs/manifest-merger-debug-report.txt +26 -34
  13. package/android/src/main/AndroidManifest.xml +22 -7
  14. package/build/components/EnhancedCameraView.web.d.ts.map +1 -1
  15. package/build/components/EnhancedCameraView.web.js +76 -21
  16. package/build/components/EnhancedCameraView.web.js.map +1 -1
  17. package/build/components/KYCElements/AdditionalDocumentsTemplate.d.ts +12 -0
  18. package/build/components/KYCElements/AdditionalDocumentsTemplate.d.ts.map +1 -0
  19. package/build/components/KYCElements/AdditionalDocumentsTemplate.js +283 -0
  20. package/build/components/KYCElements/AdditionalDocumentsTemplate.js.map +1 -0
  21. package/build/components/KYCElements/EmailVerificationTemplate.d.ts +12 -0
  22. package/build/components/KYCElements/EmailVerificationTemplate.d.ts.map +1 -0
  23. package/build/components/KYCElements/EmailVerificationTemplate.js +212 -0
  24. package/build/components/KYCElements/EmailVerificationTemplate.js.map +1 -0
  25. package/build/components/KYCElements/IDCardCapture.d.ts.map +1 -1
  26. package/build/components/KYCElements/IDCardCapture.js +216 -14
  27. package/build/components/KYCElements/IDCardCapture.js.map +1 -1
  28. package/build/components/KYCElements/OrientationVideoCapture.d.ts +2 -0
  29. package/build/components/KYCElements/OrientationVideoCapture.d.ts.map +1 -1
  30. package/build/components/KYCElements/OrientationVideoCapture.js +2 -2
  31. package/build/components/KYCElements/OrientationVideoCapture.js.map +1 -1
  32. package/build/components/KYCElements/OrientationVideoCaptureEnhanced.d.ts +2 -0
  33. package/build/components/KYCElements/OrientationVideoCaptureEnhanced.d.ts.map +1 -1
  34. package/build/components/KYCElements/OrientationVideoCaptureEnhanced.js +2 -2
  35. package/build/components/KYCElements/OrientationVideoCaptureEnhanced.js.map +1 -1
  36. package/build/components/KYCElements/OrientationVideoCaptureFinal.d.ts +2 -0
  37. package/build/components/KYCElements/OrientationVideoCaptureFinal.d.ts.map +1 -1
  38. package/build/components/KYCElements/OrientationVideoCaptureFinal.js +2 -2
  39. package/build/components/KYCElements/OrientationVideoCaptureFinal.js.map +1 -1
  40. package/build/components/KYCElements/PersonalInformationTemplate.d.ts +12 -0
  41. package/build/components/KYCElements/PersonalInformationTemplate.d.ts.map +1 -0
  42. package/build/components/KYCElements/PersonalInformationTemplate.js +120 -0
  43. package/build/components/KYCElements/PersonalInformationTemplate.js.map +1 -0
  44. package/build/components/KYCElements/PhoneVerificationTemplate.d.ts +12 -0
  45. package/build/components/KYCElements/PhoneVerificationTemplate.d.ts.map +1 -0
  46. package/build/components/KYCElements/PhoneVerificationTemplate.js +185 -0
  47. package/build/components/KYCElements/PhoneVerificationTemplate.js.map +1 -0
  48. package/build/components/KYCElements/SelfieCaptureTemplate.d.ts.map +1 -1
  49. package/build/components/KYCElements/SelfieCaptureTemplate.js +7 -3
  50. package/build/components/KYCElements/SelfieCaptureTemplate.js.map +1 -1
  51. package/build/components/KYCElements/WelcomeTemplate.js +2 -1
  52. package/build/components/KYCElements/WelcomeTemplate.js.map +1 -1
  53. package/build/components/OverLay/type.d.ts +2 -0
  54. package/build/components/OverLay/type.d.ts.map +1 -1
  55. package/build/components/OverLay/type.js.map +1 -1
  56. package/build/components/TemplateKYCExample.d.ts +10 -0
  57. package/build/components/TemplateKYCExample.d.ts.map +1 -1
  58. package/build/components/TemplateKYCExample.js +7 -30
  59. package/build/components/TemplateKYCExample.js.map +1 -1
  60. package/build/components/TemplateKYCFlowRefactored.d.ts +12 -0
  61. package/build/components/TemplateKYCFlowRefactored.d.ts.map +1 -1
  62. package/build/components/TemplateKYCFlowRefactored.js +25 -3
  63. package/build/components/TemplateKYCFlowRefactored.js.map +1 -1
  64. package/build/config/KYCConfig.d.ts +14 -0
  65. package/build/config/KYCConfig.d.ts.map +1 -0
  66. package/build/config/KYCConfig.js +26 -0
  67. package/build/config/KYCConfig.js.map +1 -0
  68. package/build/config/allowedDomains.d.ts.map +1 -1
  69. package/build/config/allowedDomains.js +4 -19
  70. package/build/config/allowedDomains.js.map +1 -1
  71. package/build/hooks/useOrientationVideo.d.ts +2 -1
  72. package/build/hooks/useOrientationVideo.d.ts.map +1 -1
  73. package/build/hooks/useOrientationVideo.js +3 -3
  74. package/build/hooks/useOrientationVideo.js.map +1 -1
  75. package/build/hooks/useTemplateKYCFlow.d.ts +18 -1
  76. package/build/hooks/useTemplateKYCFlow.d.ts.map +1 -1
  77. package/build/hooks/useTemplateKYCFlow.js +410 -56
  78. package/build/hooks/useTemplateKYCFlow.js.map +1 -1
  79. package/build/i18n/en/index.d.ts +42 -0
  80. package/build/i18n/en/index.d.ts.map +1 -1
  81. package/build/i18n/en/index.js +44 -2
  82. package/build/i18n/en/index.js.map +1 -1
  83. package/build/i18n/fr/index.d.ts +28 -0
  84. package/build/i18n/fr/index.d.ts.map +1 -1
  85. package/build/i18n/fr/index.js +30 -2
  86. package/build/i18n/fr/index.js.map +1 -1
  87. package/build/i18n/types.d.ts +2 -0
  88. package/build/i18n/types.d.ts.map +1 -1
  89. package/build/i18n/types.js.map +1 -1
  90. package/build/index.d.ts +1 -0
  91. package/build/index.d.ts.map +1 -1
  92. package/build/index.js +2 -0
  93. package/build/index.js.map +1 -1
  94. package/build/modules/api/CardAuthentification.d.ts +24 -3
  95. package/build/modules/api/CardAuthentification.d.ts.map +1 -1
  96. package/build/modules/api/CardAuthentification.js +90 -12
  97. package/build/modules/api/CardAuthentification.js.map +1 -1
  98. package/build/modules/api/KYCService.d.ts +17 -7
  99. package/build/modules/api/KYCService.d.ts.map +1 -1
  100. package/build/modules/api/KYCService.js +125 -37
  101. package/build/modules/api/KYCService.js.map +1 -1
  102. package/build/modules/api/SelfieVerification.d.ts +3 -1
  103. package/build/modules/api/SelfieVerification.d.ts.map +1 -1
  104. package/build/modules/api/SelfieVerification.js +17 -1
  105. package/build/modules/api/SelfieVerification.js.map +1 -1
  106. package/build/modules/api/TemplateService.d.ts +0 -1
  107. package/build/modules/api/TemplateService.d.ts.map +1 -1
  108. package/build/modules/api/TemplateService.js +3 -3
  109. package/build/modules/api/TemplateService.js.map +1 -1
  110. package/build/modules/camera/VisionCameraModule.web.d.ts.map +1 -1
  111. package/build/modules/camera/VisionCameraModule.web.js +27 -8
  112. package/build/modules/camera/VisionCameraModule.web.js.map +1 -1
  113. package/build/types/KYC.types.d.ts +130 -5
  114. package/build/types/KYC.types.d.ts.map +1 -1
  115. package/build/types/KYC.types.js.map +1 -1
  116. package/build/types/env.types.d.ts +13 -0
  117. package/build/types/env.types.d.ts.map +1 -0
  118. package/build/types/env.types.js +2 -0
  119. package/build/types/env.types.js.map +1 -0
  120. package/build/utils/cropByObb.d.ts +7 -0
  121. package/build/utils/cropByObb.d.ts.map +1 -1
  122. package/build/utils/cropByObb.js +20 -1
  123. package/build/utils/cropByObb.js.map +1 -1
  124. package/build/utils/deviceDetection.d.ts +6 -0
  125. package/build/utils/deviceDetection.d.ts.map +1 -0
  126. package/build/utils/deviceDetection.js +12 -0
  127. package/build/utils/deviceDetection.js.map +1 -0
  128. package/build/utils/platformAlert.d.ts.map +1 -1
  129. package/build/utils/platformAlert.js.map +1 -1
  130. package/build/utils/template-transformer.d.ts.map +1 -1
  131. package/build/utils/template-transformer.js +12 -0
  132. package/build/utils/template-transformer.js.map +1 -1
  133. package/build/web/WebKYCEntry.d.ts.map +1 -1
  134. package/build/web/WebKYCEntry.js +88 -38
  135. package/build/web/WebKYCEntry.js.map +1 -1
  136. package/package.json +1 -1
  137. package/plugin/build/index.d.ts +1 -0
  138. package/plugin/build/index.js +3 -1
  139. package/plugin/build/withRemovePermissions.d.ts +3 -0
  140. package/plugin/build/withRemovePermissions.js +67 -0
  141. package/plugin/build/withVisionCamera.js +3 -4
  142. package/plugin/src/index.ts +2 -1
  143. package/plugin/src/withRemovePermissions.js +85 -0
  144. package/plugin/src/withRemovePermissions.ts +83 -0
  145. package/plugin/src/withVisionCamera.js +3 -4
  146. package/plugin/src/withVisionCamera.ts +3 -4
  147. package/plugin/tsconfig.tsbuildinfo +1 -1
  148. package/plugin.js +6 -1
  149. package/src/components/EnhancedCameraView.web.tsx +76 -21
  150. package/src/components/KYCElements/AdditionalDocumentsTemplate.tsx +346 -0
  151. package/src/components/KYCElements/EmailVerificationTemplate.tsx +278 -0
  152. package/src/components/KYCElements/IDCardCapture.tsx +253 -21
  153. package/src/components/KYCElements/OrientationVideoCapture.tsx +4 -1
  154. package/src/components/KYCElements/OrientationVideoCaptureEnhanced.tsx +4 -1
  155. package/src/components/KYCElements/OrientationVideoCaptureFinal.tsx +4 -1
  156. package/src/components/KYCElements/PersonalInformationTemplate.tsx +158 -0
  157. package/src/components/KYCElements/PhoneVerificationTemplate.tsx +253 -0
  158. package/src/components/KYCElements/SelfieCaptureTemplate.tsx +6 -3
  159. package/src/components/KYCElements/WelcomeTemplate.tsx +2 -1
  160. package/src/components/OverLay/type.ts +2 -0
  161. package/src/components/TemplateKYCExample.tsx +35 -46
  162. package/src/components/TemplateKYCFlowRefactored.tsx +46 -2
  163. package/src/config/KYCConfig.ts +34 -0
  164. package/src/config/allowedDomains.ts +7 -26
  165. package/src/hooks/useOrientationVideo.ts +5 -4
  166. package/src/hooks/useTemplateKYCFlow.tsx +443 -56
  167. package/src/i18n/en/index.ts +46 -3
  168. package/src/i18n/fr/index.ts +31 -2
  169. package/src/i18n/types.ts +2 -0
  170. package/src/index.ts +3 -0
  171. package/src/modules/api/CardAuthentification.ts +98 -12
  172. package/src/modules/api/KYCService.ts +158 -37
  173. package/src/modules/api/SelfieVerification.ts +25 -3
  174. package/src/modules/api/TemplateService.ts +4 -4
  175. package/src/modules/camera/VisionCameraModule.web.ts +30 -12
  176. package/src/types/KYC.types.ts +153 -6
  177. package/src/types/env.types.ts +13 -0
  178. package/src/utils/cropByObb.ts +20 -1
  179. package/src/utils/deviceDetection.ts +11 -0
  180. package/src/utils/platformAlert.ts +1 -0
  181. package/src/utils/template-transformer.ts +20 -8
  182. package/src/web/WebKYCEntry.tsx +123 -61
package/plugin.js CHANGED
@@ -1,13 +1,18 @@
1
1
  const withVisionCamera = require('./plugin/build/withVisionCamera').default;
2
2
  const withLocation = require('./plugin/build/withLocation').default;
3
+ const withRemovePermissions = require('./plugin/build/withRemovePermissions').default;
3
4
 
4
- // Combine both plugins
5
+ // Combine all plugins
5
6
  const withCombinedPlugins = (config, props = {}) => {
6
7
  const { visionCamera = {}, location = {} } = props;
7
8
 
9
+ // First apply feature plugins
8
10
  config = withVisionCamera(config, visionCamera);
9
11
  config = withLocation(config, location);
10
12
 
13
+ // Then remove unwanted permissions (must be last to clean up any permissions added by dependencies)
14
+ config = withRemovePermissions(config);
15
+
11
16
  return config;
12
17
  };
13
18
 
@@ -27,6 +27,7 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
27
27
  videoDuration = 10,
28
28
  onSilentCapture,
29
29
  silentCaptureResult,
30
+ captureStabilizationDelayMs = 2500,
30
31
  }) => {
31
32
  const { t } = useI18n();
32
33
 
@@ -63,6 +64,24 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
63
64
  };
64
65
  }, [stream]);
65
66
 
67
+ // Build video constraints for quality (min + ideal improve Android/Samsung web quality; higher ideal for documents)
68
+ const getVideoConstraints = useCallback((): MediaTrackConstraints => {
69
+ const isHigh = quality === 'high';
70
+ const isMedium = quality === 'medium';
71
+ return {
72
+ facingMode: cameraType === 'front' ? 'user' : 'environment',
73
+ // min forces Android Chrome to use at least this resolution (avoids 640x480 default)
74
+ width: {
75
+ min: isHigh ? 1280 : isMedium ? 960 : 640,
76
+ ideal: isHigh ? 2560 : isMedium ? 1280 : 640,
77
+ },
78
+ height: {
79
+ min: isHigh ? 720 : isMedium ? 540 : 480,
80
+ ideal: isHigh ? 1440 : isMedium ? 720 : 480,
81
+ },
82
+ };
83
+ }, [cameraType, quality]);
84
+
66
85
  const checkPermissions = async () => {
67
86
  try {
68
87
  if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
@@ -70,8 +89,11 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
70
89
  return;
71
90
  }
72
91
 
73
- // Test camera permission
74
- const stream = await navigator.mediaDevices.getUserMedia({ video: true });
92
+ // Use same constraints as startCamera so Android Chrome doesn't cache low resolution (e.g. 640x480)
93
+ const stream = await navigator.mediaDevices.getUserMedia({
94
+ video: getVideoConstraints(),
95
+ audio: enableVideo,
96
+ });
75
97
  stream.getTracks().forEach(track => track.stop());
76
98
 
77
99
  setHasPermission(true);
@@ -88,16 +110,32 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
88
110
  stream.getTracks().forEach(track => track.stop());
89
111
  }
90
112
 
91
- const constraints = {
92
- video: {
93
- facingMode: cameraType === 'front' ? 'user' : 'environment',
94
- width: { ideal: quality === 'high' ? 1920 : quality === 'medium' ? 1280 : 640 },
95
- height: { ideal: quality === 'high' ? 1080 : quality === 'medium' ? 720 : 480 },
96
- },
113
+ const constraints: MediaStreamConstraints = {
114
+ video: getVideoConstraints(),
97
115
  audio: enableVideo,
98
116
  };
99
117
 
100
- const newStream = await navigator.mediaDevices.getUserMedia(constraints);
118
+ let newStream: MediaStream;
119
+ try {
120
+ newStream = await navigator.mediaDevices.getUserMedia(constraints);
121
+ } catch (err) {
122
+ const name = err instanceof Error ? err.name : '';
123
+ // On some Android devices, min constraints can fail; fallback to ideal only
124
+ if (name === 'OverconstrainedError') {
125
+ const fallbackConstraints: MediaStreamConstraints = {
126
+ video: {
127
+ facingMode: cameraType === 'front' ? 'user' : 'environment',
128
+ width: { ideal: quality === 'high' ? 2560 : quality === 'medium' ? 1280 : 640 },
129
+ height: { ideal: quality === 'high' ? 1440 : quality === 'medium' ? 720 : 480 },
130
+ },
131
+ audio: enableVideo,
132
+ };
133
+ newStream = await navigator.mediaDevices.getUserMedia(fallbackConstraints);
134
+ } else {
135
+ throw err;
136
+ }
137
+ }
138
+
101
139
  setStream(newStream);
102
140
 
103
141
  if (videoRef.current) {
@@ -159,11 +197,19 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
159
197
  canvas.width = video.videoWidth;
160
198
  canvas.height = video.videoHeight;
161
199
 
162
- // Draw the current video frame to canvas
163
- context.drawImage(video, 0, 0);
200
+ // For front camera: draw flipped so captured image matches non-mirrored preview (no mirror confusion)
201
+ if (cameraType === 'front') {
202
+ context.save();
203
+ context.translate(canvas.width, 0);
204
+ context.scale(-1, 1);
205
+ context.drawImage(video, 0, 0);
206
+ context.restore();
207
+ } else {
208
+ context.drawImage(video, 0, 0);
209
+ }
164
210
 
165
- // Convert to base64
166
- const imageDataUrl = canvas.toDataURL('image/jpeg', 0.8);
211
+ // Convert to base64 (0.95 for document/ID capture clarity; backend can exploit text better)
212
+ const imageDataUrl = canvas.toDataURL('image/jpeg', 0.95);
167
213
 
168
214
  onSilentCapture?.({
169
215
  success: true,
@@ -176,21 +222,29 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
176
222
  error: error instanceof Error ? error.message : 'Failed to capture photo',
177
223
  });
178
224
  }
179
- }, [isInitialized, onError, onSilentCapture]);
225
+ }, [isInitialized, cameraType, onError, onSilentCapture]);
180
226
 
181
227
 
182
- // Automatically take a silent photo every 5 seconds when ready
228
+ // Stabilization delay then auto-capture every 5s; stop as soon as capture is validated (no more new captures)
183
229
  useEffect(() => {
184
- if (!showCamera || !isInitialized) {
230
+ if (!showCamera || !isInitialized || silentCaptureResult?.success) {
185
231
  return;
186
232
  }
187
233
 
188
- const intervalId = setInterval(() => {
234
+ const delayMs = Math.max(0, captureStabilizationDelayMs);
235
+ const intervalMs = 5000;
236
+ let intervalId: ReturnType<typeof setInterval> | null = null;
237
+
238
+ const timeoutId = setTimeout(() => {
189
239
  captureSilentPhoto();
190
- }, 5000);
240
+ intervalId = setInterval(captureSilentPhoto, intervalMs);
241
+ }, delayMs);
191
242
 
192
- return () => clearInterval(intervalId);
193
- }, [showCamera, isInitialized, captureSilentPhoto]);
243
+ return () => {
244
+ clearTimeout(timeoutId);
245
+ if (intervalId) clearInterval(intervalId);
246
+ };
247
+ }, [showCamera, isInitialized, captureStabilizationDelayMs, captureSilentPhoto, silentCaptureResult?.success]);
194
248
 
195
249
  const startVideoRecording = useCallback(async () => {
196
250
  try {
@@ -299,7 +353,7 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
299
353
 
300
354
  return (
301
355
  <View style={[styles.container, style]}>
302
- {/* Video element */}
356
+ {/* Video element; no mirror for front camera so preview matches final photo and doesn't confuse users */}
303
357
  <video
304
358
  ref={videoRef}
305
359
  style={{
@@ -307,6 +361,7 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
307
361
  width: '100%',
308
362
  height: '100%',
309
363
  objectFit: 'cover',
364
+ transform: cameraType === 'front' ? 'scaleX(-1)' : undefined,
310
365
  }}
311
366
  autoPlay
312
367
  playsInline
@@ -0,0 +1,346 @@
1
+ import React, { useState } from 'react';
2
+ import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Image } from 'react-native';
3
+ import { TemplateComponent, AdditionalDocumentsConfig, 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 NativeCameraModule from '../../modules/camera/NativeCameraModule';
8
+ import { useKYCStore } from '../../stores/kycStore';
9
+ import { showAlert } from '../../utils/platformAlert';
10
+
11
+ interface AdditionalDocumentsTemplateProps {
12
+ component: TemplateComponent;
13
+ value?: any;
14
+ onValueChange: (data: any) => void;
15
+ error?: string;
16
+ language?: string;
17
+ }
18
+
19
+ interface UploadedFile {
20
+ uri: string;
21
+ path: string;
22
+ name: string;
23
+ size: number;
24
+ }
25
+
26
+ export const AdditionalDocumentsTemplate: React.FC<AdditionalDocumentsTemplateProps> = ({
27
+ component,
28
+ value,
29
+ onValueChange,
30
+ error,
31
+ language = 'en',
32
+ }) => {
33
+ const { actions, getLocalizedText } = useTemplateKYCFlowContext();
34
+ const { t } = useI18n();
35
+ const { setProcessing } = useKYCStore();
36
+ const config = component.config as AdditionalDocumentsConfig;
37
+
38
+ // State
39
+ const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
40
+ const [isUploading, setIsUploading] = useState(false);
41
+
42
+ const title = getLocalizedText(component.labels as LocalizedText);
43
+ const instructions = getLocalizedText(component.instructions as LocalizedText);
44
+ const buttonText = getLocalizedText((component.ui as any).buttonText) || t('common.continue');
45
+
46
+ // Parse max size from config string (e.g. "5MB" -> bytes)
47
+ const parseMaxSize = (sizeStr: string): number => {
48
+ if (!sizeStr) return 10 * 1024 * 1024; // Default 10MB
49
+ const value = parseFloat(sizeStr);
50
+ if (sizeStr.toLowerCase().includes('kb')) return value * 1024;
51
+ if (sizeStr.toLowerCase().includes('gb')) return value * 1024 * 1024 * 1024;
52
+ return value * 1024 * 1024; // Default to MB if only number or MB
53
+ };
54
+
55
+ const maxSizeBytes = parseMaxSize(config.maxSizeEach);
56
+ const maxFiles = config.maxDocuments || 3;
57
+
58
+ const pickDocument = async () => {
59
+ if (uploadedFiles.length >= maxFiles) {
60
+ showAlert(
61
+ t('common.limitReached') || 'Limit Reached',
62
+ t('errors.maxFilesReached', { max: maxFiles }) || `You can only upload ${maxFiles} files.`
63
+ );
64
+ return;
65
+ }
66
+
67
+ try {
68
+ setIsUploading(true);
69
+ setProcessing(true);
70
+
71
+ // Default allowed types since AdditionalDocumentsConfig structure is complex for categories
72
+ const allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
73
+
74
+ const result = await NativeCameraModule.openFilePicker(allowedTypes);
75
+
76
+ if (result.success && result.uri && result.path) {
77
+ const fileName = result.path.split('/').pop() || 'Document';
78
+ const fileSize = (result as any).size || 0;
79
+
80
+ if (fileSize > maxSizeBytes) {
81
+ showAlert(
82
+ t('errors.fileTooLarge') || 'File too large',
83
+ t('errors.maxFileSize', { size: config.maxSizeEach }) || `Max file size is ${config.maxSizeEach}`
84
+ );
85
+ return;
86
+ }
87
+
88
+ const newFile: UploadedFile = {
89
+ uri: result.uri,
90
+ path: result.path,
91
+ name: fileName,
92
+ size: fileSize
93
+ };
94
+
95
+ const updatedFiles = [...uploadedFiles, newFile];
96
+ setUploadedFiles(updatedFiles);
97
+ onValueChange({ documents: updatedFiles }); // Matching expected data structure
98
+ } else if (result.error) {
99
+ // Ignore cancellation errors or show generic if needed
100
+ if (result.error !== 'Canceled') {
101
+ showAlert(t('common.error') || 'Error', result.error);
102
+ }
103
+ }
104
+ } catch (error) {
105
+ console.error('Error selecting file:', error);
106
+ showAlert(t('common.error') || 'Error', t('errors.unknownError') || 'Failed to select file');
107
+ } finally {
108
+ setIsUploading(false);
109
+ setProcessing(false);
110
+ }
111
+ };
112
+
113
+ const removeFile = (index: number) => {
114
+ const updatedFiles = uploadedFiles.filter((_, i) => i !== index);
115
+ setUploadedFiles(updatedFiles);
116
+ onValueChange({ documents: updatedFiles });
117
+ };
118
+
119
+ const handleContinue = () => {
120
+ actions.nextComponent();
121
+ };
122
+
123
+ // Helpers for UI
124
+ const getFileIcon = (fileName: string) => {
125
+ const extension = fileName.split('.').pop()?.toLowerCase();
126
+ switch (extension) {
127
+ case 'pdf': return '📄';
128
+ case 'jpg':
129
+ case 'jpeg':
130
+ case 'png': return '🖼️';
131
+ default: return '📎';
132
+ }
133
+ };
134
+
135
+ const isImageFile = (fileName: string): boolean => {
136
+ const extension = fileName.split('.').pop()?.toLowerCase();
137
+ return ['jpg', 'jpeg', 'png', 'webp'].includes(extension || '');
138
+ };
139
+
140
+ const formatFileSize = (bytes: number): string => {
141
+ if (bytes === 0) return '0 Bytes';
142
+ const k = 1024;
143
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
144
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
145
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
146
+ };
147
+
148
+ return (
149
+ <View style={styles.container}>
150
+ <Text style={styles.title}>{title}</Text>
151
+ <Text style={styles.instructions}>{instructions}</Text>
152
+
153
+ <View style={styles.uploadArea}>
154
+ <TouchableOpacity
155
+ style={[
156
+ styles.uploadButton,
157
+ isUploading && styles.uploadButtonDisabled,
158
+ { borderColor: component.ui.themeColor as string || '#2DBD60' }
159
+ ]}
160
+ onPress={pickDocument}
161
+ disabled={isUploading}
162
+ >
163
+ <Text style={styles.uploadIcon}>📁</Text>
164
+ <Text style={[
165
+ styles.uploadText,
166
+ { color: component.ui.themeColor as string || '#2DBD60' }
167
+ ]}>
168
+ {isUploading ? (t('common.processing') || 'Processing...') : (t('kyc.additionalDocs.add') || 'Select Files')}
169
+ </Text>
170
+ <Text style={styles.uploadSubtext}>
171
+ {t('kyc.additionalDocs.maxSize', { size: config.maxSizeEach }) || `Max size: ${config.maxSizeEach}`}
172
+ </Text>
173
+ </TouchableOpacity>
174
+ </View>
175
+
176
+ {uploadedFiles.length > 0 && (
177
+ <View style={styles.filesContainer}>
178
+ <Text style={styles.subTitle}>
179
+ {t('kyc.additionalDocs.uploaded') || 'Uploaded Documents'} ({uploadedFiles.length}/{maxFiles})
180
+ </Text>
181
+ <ScrollView style={styles.filesList} showsVerticalScrollIndicator={false}>
182
+ {uploadedFiles.map((file, index) => (
183
+ <View key={index} style={styles.fileItem}>
184
+ {isImageFile(file.name) ? (
185
+ <Image source={{ uri: file.uri }} style={styles.fileThumbnail} />
186
+ ) : (
187
+ <View style={styles.fileIcon}>
188
+ <Text style={styles.fileIconText}>{getFileIcon(file.name)}</Text>
189
+ </View>
190
+ )}
191
+ <View style={styles.fileInfo}>
192
+ <Text style={styles.fileName} numberOfLines={1}>{file.name}</Text>
193
+ <Text style={styles.fileSize}>{formatFileSize(file.size)}</Text>
194
+ </View>
195
+ <TouchableOpacity
196
+ style={styles.removeButton}
197
+ onPress={() => removeFile(index)}
198
+ >
199
+ <Text style={styles.removeButtonText}>✕</Text>
200
+ </TouchableOpacity>
201
+ </View>
202
+ ))}
203
+ </ScrollView>
204
+ </View>
205
+ )}
206
+
207
+ {error && <Text style={styles.errorText}>{error}</Text>}
208
+
209
+ <Button
210
+ title={buttonText}
211
+ onPress={handleContinue}
212
+ fullWidth
213
+ style={styles.button}
214
+ disabled={config.required && uploadedFiles.length < (config.minDocuments || 1)}
215
+ />
216
+ </View>
217
+ );
218
+ };
219
+
220
+ const styles = StyleSheet.create({
221
+ container: {
222
+ padding: 20,
223
+ backgroundColor: 'white',
224
+ borderRadius: 12,
225
+ margin: 16,
226
+ shadowColor: '#000',
227
+ shadowOffset: { width: 0, height: 2 },
228
+ shadowOpacity: 0.1,
229
+ shadowRadius: 4,
230
+ elevation: 3,
231
+ width: '95%',
232
+ },
233
+ title: {
234
+ fontSize: 22,
235
+ fontWeight: 'bold',
236
+ marginBottom: 10,
237
+ color: '#333',
238
+ },
239
+ instructions: {
240
+ fontSize: 16,
241
+ color: '#666',
242
+ marginBottom: 20,
243
+ lineHeight: 22,
244
+ },
245
+ subTitle: {
246
+ fontSize: 16,
247
+ fontWeight: '600',
248
+ marginBottom: 10,
249
+ color: '#333',
250
+ },
251
+ uploadArea: {
252
+ marginBottom: 20,
253
+ },
254
+ uploadButton: {
255
+ borderWidth: 2,
256
+ borderStyle: 'dashed',
257
+ borderRadius: 12,
258
+ padding: 24,
259
+ alignItems: 'center',
260
+ backgroundColor: '#f0f9f0',
261
+ },
262
+ uploadButtonDisabled: {
263
+ opacity: 0.6,
264
+ },
265
+ uploadIcon: {
266
+ fontSize: 32,
267
+ marginBottom: 8,
268
+ },
269
+ uploadText: {
270
+ fontSize: 16,
271
+ fontWeight: '600',
272
+ marginBottom: 4,
273
+ },
274
+ uploadSubtext: {
275
+ fontSize: 12,
276
+ color: '#666',
277
+ },
278
+ filesContainer: {
279
+ flex: 1,
280
+ marginBottom: 10,
281
+ },
282
+ filesList: {
283
+ maxHeight: 200,
284
+ },
285
+ fileItem: {
286
+ flexDirection: 'row',
287
+ alignItems: 'center',
288
+ padding: 10,
289
+ backgroundColor: '#f9f9f9',
290
+ borderRadius: 8,
291
+ marginBottom: 8,
292
+ borderWidth: 1,
293
+ borderColor: '#eee',
294
+ },
295
+ fileThumbnail: {
296
+ width: 36,
297
+ height: 36,
298
+ borderRadius: 4,
299
+ marginRight: 10,
300
+ },
301
+ fileIcon: {
302
+ width: 36,
303
+ height: 36,
304
+ borderRadius: 4,
305
+ backgroundColor: '#eee',
306
+ justifyContent: 'center',
307
+ alignItems: 'center',
308
+ marginRight: 10,
309
+ },
310
+ fileIconText: {
311
+ fontSize: 18,
312
+ },
313
+ fileInfo: {
314
+ flex: 1,
315
+ },
316
+ fileName: {
317
+ fontSize: 14,
318
+ fontWeight: '500',
319
+ color: '#333',
320
+ },
321
+ fileSize: {
322
+ fontSize: 12,
323
+ color: '#888',
324
+ },
325
+ removeButton: {
326
+ width: 24,
327
+ height: 24,
328
+ borderRadius: 12,
329
+ backgroundColor: '#ff4444',
330
+ justifyContent: 'center',
331
+ alignItems: 'center',
332
+ marginLeft: 8,
333
+ },
334
+ removeButtonText: {
335
+ color: 'white',
336
+ fontSize: 12,
337
+ fontWeight: 'bold',
338
+ },
339
+ errorText: {
340
+ color: 'red',
341
+ marginBottom: 10,
342
+ },
343
+ button: {
344
+ marginTop: 10,
345
+ },
346
+ });