@transfergratis/react-native-sdk 0.1.23 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml +12 -5
- package/android/build/intermediates/aar_main_jar/debug/syncDebugLibJars/classes.jar +0 -0
- package/android/build/intermediates/annotations_typedef_file/debug/extractDebugAnnotations/typedefs.txt +0 -0
- package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +1 -1
- package/android/build/intermediates/incremental/debug-mergeJavaRes/merge-state +0 -0
- package/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt +61 -59
- package/android/build/intermediates/merged_java_res/debug/mergeDebugJavaResource/feature-transfergratis-react-native-sdk.jar +0 -0
- package/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml +12 -5
- package/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin +0 -0
- package/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin +0 -0
- package/android/build/outputs/aar/transfergratis-react-native-sdk-debug.aar +0 -0
- package/android/build/outputs/logs/manifest-merger-debug-report.txt +26 -34
- package/android/src/main/AndroidManifest.xml +13 -5
- 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/AdditionalDocumentsTemplate.d.ts +12 -0
- package/build/components/KYCElements/AdditionalDocumentsTemplate.d.ts.map +1 -0
- package/build/components/KYCElements/AdditionalDocumentsTemplate.js +283 -0
- package/build/components/KYCElements/AdditionalDocumentsTemplate.js.map +1 -0
- package/build/components/KYCElements/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/EmailVerificationTemplate.d.ts +12 -0
- package/build/components/KYCElements/EmailVerificationTemplate.d.ts.map +1 -0
- package/build/components/KYCElements/EmailVerificationTemplate.js +193 -0
- package/build/components/KYCElements/EmailVerificationTemplate.js.map +1 -0
- 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 +356 -227
- 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.d.ts +2 -0
- package/build/components/KYCElements/OrientationVideoCapture.d.ts.map +1 -1
- package/build/components/KYCElements/OrientationVideoCapture.js +5 -4
- package/build/components/KYCElements/OrientationVideoCapture.js.map +1 -1
- package/build/components/KYCElements/OrientationVideoCaptureEnhanced.d.ts +2 -0
- package/build/components/KYCElements/OrientationVideoCaptureEnhanced.d.ts.map +1 -1
- package/build/components/KYCElements/OrientationVideoCaptureEnhanced.js +5 -4
- package/build/components/KYCElements/OrientationVideoCaptureEnhanced.js.map +1 -1
- package/build/components/KYCElements/OrientationVideoCaptureFinal.d.ts +2 -0
- package/build/components/KYCElements/OrientationVideoCaptureFinal.d.ts.map +1 -1
- package/build/components/KYCElements/OrientationVideoCaptureFinal.js +5 -4
- package/build/components/KYCElements/OrientationVideoCaptureFinal.js.map +1 -1
- package/build/components/KYCElements/PersonalInformationTemplate.d.ts +12 -0
- package/build/components/KYCElements/PersonalInformationTemplate.d.ts.map +1 -0
- package/build/components/KYCElements/PersonalInformationTemplate.js +120 -0
- package/build/components/KYCElements/PersonalInformationTemplate.js.map +1 -0
- package/build/components/KYCElements/PhoneVerificationTemplate.d.ts +12 -0
- package/build/components/KYCElements/PhoneVerificationTemplate.d.ts.map +1 -0
- package/build/components/KYCElements/PhoneVerificationTemplate.js +185 -0
- package/build/components/KYCElements/PhoneVerificationTemplate.js.map +1 -0
- package/build/components/KYCElements/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 +189 -42
- 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 +8 -2
- package/build/components/TemplateKYCExample.d.ts.map +1 -1
- package/build/components/TemplateKYCExample.js +10 -97
- package/build/components/TemplateKYCExample.js.map +1 -1
- package/build/components/TemplateKYCFlowRefactored.d.ts +6 -1
- package/build/components/TemplateKYCFlowRefactored.d.ts.map +1 -1
- package/build/components/TemplateKYCFlowRefactored.js +108 -11
- 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/KYCConfig.d.ts +14 -0
- package/build/config/KYCConfig.d.ts.map +1 -0
- package/build/config/KYCConfig.js +26 -0
- package/build/config/KYCConfig.js.map +1 -0
- package/build/config/allowedDomains.d.ts +30 -0
- package/build/config/allowedDomains.d.ts.map +1 -0
- package/build/config/allowedDomains.js +112 -0
- package/build/config/allowedDomains.js.map +1 -0
- package/build/hooks/useOrientationVideo.d.ts +2 -1
- package/build/hooks/useOrientationVideo.d.ts.map +1 -1
- package/build/hooks/useOrientationVideo.js +3 -3
- package/build/hooks/useOrientationVideo.js.map +1 -1
- package/build/hooks/useTemplateKYCFlow.d.ts +6 -1
- package/build/hooks/useTemplateKYCFlow.d.ts.map +1 -1
- package/build/hooks/useTemplateKYCFlow.js +317 -34
- 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 +49 -0
- package/build/i18n/en/index.d.ts.map +1 -1
- package/build/i18n/en/index.js +50 -1
- package/build/i18n/en/index.js.map +1 -1
- package/build/i18n/fr/index.d.ts +35 -0
- package/build/i18n/fr/index.d.ts.map +1 -1
- package/build/i18n/fr/index.js +36 -1
- package/build/i18n/fr/index.js.map +1 -1
- package/build/index.d.ts +6 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10 -0
- package/build/index.js.map +1 -1
- package/build/modules/api/CardAuthentification.d.ts +24 -3
- package/build/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/modules/api/CardAuthentification.js +69 -10
- package/build/modules/api/CardAuthentification.js.map +1 -1
- package/build/modules/api/KYCService.d.ts +7 -7
- package/build/modules/api/KYCService.d.ts.map +1 -1
- package/build/modules/api/KYCService.js +108 -39
- package/build/modules/api/KYCService.js.map +1 -1
- package/build/modules/api/SelfieVerification.d.ts +3 -1
- package/build/modules/api/SelfieVerification.d.ts.map +1 -1
- package/build/modules/api/SelfieVerification.js +17 -1
- package/build/modules/api/SelfieVerification.js.map +1 -1
- package/build/modules/api/TemplateService.d.ts +44 -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/types/KYC.types.d.ts +265 -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/types/env.types.d.ts +13 -0
- package/build/types/env.types.d.ts.map +1 -0
- package/build/types/env.types.js +2 -0
- package/build/types/env.types.js.map +1 -0
- package/build/utils/cropByObb.d.ts +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/deviceDetection.d.ts +6 -0
- package/build/utils/deviceDetection.d.ts.map +1 -0
- package/build/utils/deviceDetection.js +12 -0
- package/build/utils/deviceDetection.js.map +1 -0
- package/build/utils/platformAlert.d.ts +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 +365 -0
- package/build/utils/template-transformer.js.map +1 -0
- package/build/web/WebKYCEntry.d.ts.map +1 -1
- package/build/web/WebKYCEntry.js +158 -32
- package/build/web/WebKYCEntry.js.map +1 -1
- package/package.json +1 -1
- package/plugin/build/withVisionCamera.js +3 -4
- package/plugin/src/withVisionCamera.js +3 -4
- package/plugin/src/withVisionCamera.ts +3 -4
- package/src/components/EnhancedCameraView.tsx +31 -2
- package/src/components/EnhancedCameraView.web.tsx +24 -0
- package/src/components/KYCElements/AdditionalDocumentsTemplate.tsx +346 -0
- package/src/components/KYCElements/CameraCapture.tsx +4 -3
- package/src/components/KYCElements/CountrySelectionTemplate.tsx +410 -113
- package/src/components/KYCElements/EmailVerificationTemplate.tsx +264 -0
- package/src/components/KYCElements/FileUpload.tsx +5 -4
- package/src/components/KYCElements/FileUploadTemplate.tsx +5 -4
- package/src/components/KYCElements/IDCardCapture.tsx +397 -254
- package/src/components/KYCElements/LocationCaptureTemplate.tsx +95 -44
- package/src/components/KYCElements/OrientationVideoCapture.tsx +6 -3
- package/src/components/KYCElements/OrientationVideoCaptureEnhanced.tsx +6 -3
- package/src/components/KYCElements/OrientationVideoCaptureFinal.tsx +6 -3
- package/src/components/KYCElements/PersonalInformationTemplate.tsx +158 -0
- package/src/components/KYCElements/PhoneVerificationTemplate.tsx +253 -0
- package/src/components/KYCElements/SelfieCapture.tsx +4 -3
- package/src/components/KYCElements/SelfieCaptureTemplate.tsx +201 -44
- package/src/components/KYCElements/WelcomeTemplate.tsx +289 -0
- package/src/components/TemplateKYCExample.tsx +37 -108
- package/src/components/TemplateKYCFlowRefactored.tsx +148 -12
- package/src/components/example/DynamicTemplateExample.tsx +289 -0
- package/src/config/KYCConfig.ts +34 -0
- package/src/config/allowedDomains.ts +133 -0
- package/src/hooks/useOrientationVideo.ts +5 -4
- package/src/hooks/useTemplateKYCFlow.tsx +347 -32
- package/src/hooks/useTemplateLoader.ts +102 -0
- package/src/i18n/en/index.ts +53 -2
- package/src/i18n/fr/index.ts +37 -1
- package/src/index.ts +14 -0
- package/src/modules/api/CardAuthentification.ts +76 -11
- package/src/modules/api/KYCService.ts +129 -45
- package/src/modules/api/SelfieVerification.ts +25 -3
- package/src/modules/api/TemplateService.ts +167 -0
- package/src/types/KYC.types.ts +331 -3
- package/src/types/env.types.ts +13 -0
- package/src/utils/cropByObb.ts +83 -3
- package/src/utils/deviceDetection.ts +11 -0
- package/src/utils/platformAlert.ts +86 -0
- package/src/utils/template-transformer.ts +445 -0
- package/src/web/WebKYCEntry.tsx +199 -50
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
OrientationVideoResult,
|
|
7
7
|
OrientationType
|
|
8
8
|
} from '../types/KYC.types';
|
|
9
|
+
import { KycEnvironment } from '../types/env.types';
|
|
9
10
|
import kycService from '../modules/api/KYCService';
|
|
10
11
|
|
|
11
12
|
export interface UseOrientationVideoReturn {
|
|
@@ -15,8 +16,8 @@ export interface UseOrientationVideoReturn {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export const useOrientationVideo = (
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
config?: Partial<OrientationVideoConfig>,
|
|
20
|
+
env: KycEnvironment = 'PRODUCTION'
|
|
20
21
|
): UseOrientationVideoReturn => {
|
|
21
22
|
const defaultConfig: OrientationVideoConfig = {
|
|
22
23
|
duration: 10,
|
|
@@ -66,7 +67,7 @@ export const useOrientationVideo = (
|
|
|
66
67
|
setState(prev => ({ ...prev, isProcessing: true, error: null }));
|
|
67
68
|
|
|
68
69
|
try {
|
|
69
|
-
const result = await kycService.processOrientationVideo(state.recordedVideo);
|
|
70
|
+
const result = await kycService.processOrientationVideo(state.recordedVideo, env);
|
|
70
71
|
|
|
71
72
|
if (result.success && result.data) {
|
|
72
73
|
setState(prev => ({
|
|
@@ -93,7 +94,7 @@ export const useOrientationVideo = (
|
|
|
93
94
|
}));
|
|
94
95
|
throw error;
|
|
95
96
|
}
|
|
96
|
-
}, [state.recordedVideo, state.isProcessing,
|
|
97
|
+
}, [state.recordedVideo, state.isProcessing, env]);
|
|
97
98
|
|
|
98
99
|
const retake = useCallback(() => {
|
|
99
100
|
if (state.retakeCount >= finalConfig.maxRetakes) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useCallback, useMemo, createContext, useContext, ReactNode, useEffect } from 'react';
|
|
2
2
|
import { KYCTemplate, TemplateState, TemplateActions, UseTemplateReturn, TemplateComponent, GovernmentDocumentType, VerificationState } from '../types/KYC.types';
|
|
3
|
+
import { KycEnvironment } from '../types/env.types';
|
|
3
4
|
import kycService, { authentification, truncateFields } from '../modules/api/KYCService';
|
|
4
5
|
import useI18n from './useI18n';
|
|
5
6
|
import { logger } from '../utils/logger';
|
|
@@ -15,6 +16,7 @@ interface TemplateKYCFlowContextType {
|
|
|
15
16
|
isComplete: boolean;
|
|
16
17
|
getLocalizedText: (text: { en: string; fr: string;[key: string]: string }) => string;
|
|
17
18
|
initializeSession: () => Promise<void>;
|
|
19
|
+
env: KycEnvironment;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
const TemplateKYCFlowContext = createContext<TemplateKYCFlowContextType | undefined>(undefined);
|
|
@@ -28,6 +30,9 @@ interface TemplateKYCFlowProviderProps {
|
|
|
28
30
|
onCancel?: () => void;
|
|
29
31
|
initialLanguage?: string;
|
|
30
32
|
apiKey?: string;
|
|
33
|
+
env?: KycEnvironment;
|
|
34
|
+
existingSessionId?: string;
|
|
35
|
+
initialStep?: number;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export const TemplateKYCFlowProvider: React.FC<TemplateKYCFlowProviderProps> = ({
|
|
@@ -38,8 +43,10 @@ export const TemplateKYCFlowProvider: React.FC<TemplateKYCFlowProviderProps> = (
|
|
|
38
43
|
onCancel,
|
|
39
44
|
initialLanguage = 'en',
|
|
40
45
|
apiKey,
|
|
46
|
+
env = 'PRODUCTION',
|
|
47
|
+
existingSessionId,
|
|
41
48
|
}) => {
|
|
42
|
-
const hookResult = useTemplateKYCFlow(template, onComplete, onError, onCancel, initialLanguage, apiKey);
|
|
49
|
+
const hookResult = useTemplateKYCFlow(template, onComplete, onError, onCancel, initialLanguage, apiKey, env, existingSessionId);
|
|
43
50
|
|
|
44
51
|
return (
|
|
45
52
|
<TemplateKYCFlowContext.Provider value={hookResult}>
|
|
@@ -64,6 +71,9 @@ export const useTemplateKYCFlow = (
|
|
|
64
71
|
onCancel?: () => void,
|
|
65
72
|
initialLanguage: string = 'en',
|
|
66
73
|
apiKey?: string,
|
|
74
|
+
env: KycEnvironment = 'PRODUCTION',
|
|
75
|
+
existingSessionId?: string,
|
|
76
|
+
initialStep?: number,
|
|
67
77
|
): UseTemplateReturn => {
|
|
68
78
|
|
|
69
79
|
const { setLocale } = useI18n();
|
|
@@ -131,29 +141,286 @@ export const useTemplateKYCFlow = (
|
|
|
131
141
|
const templateWithReviewAndVerification = useMemo(() => ensureVerificationProgressStep(templateWithReview), [templateWithReview, ensureVerificationProgressStep, apiKey]);
|
|
132
142
|
|
|
133
143
|
// État initial du flux
|
|
134
|
-
const buildInitialState = (): TemplateState =>
|
|
135
|
-
template
|
|
136
|
-
|
|
137
|
-
completedComponents: []
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
const buildInitialState = (): TemplateState => {
|
|
145
|
+
// Valider initialStep pour s'assurer qu'il est dans les limites du template
|
|
146
|
+
let validInitialStep = 0;
|
|
147
|
+
let completedComponents: number[] = [];
|
|
148
|
+
|
|
149
|
+
logger.log('buildInitialState called', { initialStep, existingSessionId });
|
|
150
|
+
|
|
151
|
+
if (initialStep !== undefined && initialStep >= 0) {
|
|
152
|
+
const maxIndex = templateWithReviewAndVerification.components.length - 1;
|
|
153
|
+
const requestedStep = Math.min(initialStep, maxIndex);
|
|
154
|
+
const requestedComponent = templateWithReviewAndVerification.components[requestedStep];
|
|
155
|
+
|
|
156
|
+
logger.log('Processing initialStep', {
|
|
157
|
+
initialStep,
|
|
158
|
+
requestedStep,
|
|
159
|
+
maxIndex,
|
|
160
|
+
componentType: requestedComponent?.type,
|
|
161
|
+
componentId: requestedComponent?.id
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Si on reprend à l'étape id_card, on peut rester à id_card si on a une session existante
|
|
165
|
+
// car les données de country_selection peuvent être chargées depuis la session
|
|
166
|
+
if (requestedComponent?.type === 'id_card') {
|
|
167
|
+
// Si on a une session existante, on peut rester à id_card et charger les données
|
|
168
|
+
if (existingSessionId) {
|
|
169
|
+
logger.log('id_card with existing session - staying at id_card');
|
|
170
|
+
validInitialStep = requestedStep;
|
|
171
|
+
// Marquer les composants précédents comme complétés
|
|
172
|
+
if (validInitialStep > 0) {
|
|
173
|
+
completedComponents = templateWithReviewAndVerification.components
|
|
174
|
+
.slice(0, validInitialStep)
|
|
175
|
+
.map(component => component.id);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
// Si pas de session, revenir à country_selection pour refaire le choix
|
|
179
|
+
const countrySelectionIndex = templateWithReviewAndVerification.components.findIndex(
|
|
180
|
+
c => c.type === 'country_selection'
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
logger.log('id_card without session - going back to country_selection', { countrySelectionIndex });
|
|
184
|
+
|
|
185
|
+
if (countrySelectionIndex >= 0) {
|
|
186
|
+
validInitialStep = countrySelectionIndex;
|
|
187
|
+
// Marquer les composants avant country_selection comme complétés
|
|
188
|
+
if (countrySelectionIndex > 0) {
|
|
189
|
+
completedComponents = templateWithReviewAndVerification.components
|
|
190
|
+
.slice(0, countrySelectionIndex)
|
|
191
|
+
.map(component => component.id);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
// Si pas de country_selection, commencer au début
|
|
195
|
+
validInitialStep = 0;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else if (requestedComponent?.type === 'review_submit') {
|
|
199
|
+
// Si on reprend au review_submit, on ne marque pas les composants précédents
|
|
200
|
+
// pour permettre à l'utilisateur de revenir en arrière et vérifier/modifier les données
|
|
201
|
+
validInitialStep = requestedStep;
|
|
202
|
+
// Ne pas marquer les composants précédents comme complétés
|
|
203
|
+
completedComponents = [];
|
|
204
|
+
} else {
|
|
205
|
+
// Pour les autres composants (selfie, etc.), commencer directement à l'étape demandée
|
|
206
|
+
validInitialStep = requestedStep;
|
|
207
|
+
|
|
208
|
+
// Marquer tous les composants précédents comme complétés
|
|
209
|
+
// Cela permet à l'utilisateur de continuer sans refaire les étapes précédentes
|
|
210
|
+
if (validInitialStep > 0) {
|
|
211
|
+
completedComponents = templateWithReviewAndVerification.components
|
|
212
|
+
.slice(0, validInitialStep)
|
|
213
|
+
.map(component => component.id);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
logger.log('Final initial state', {
|
|
218
|
+
validInitialStep,
|
|
219
|
+
completedComponentsCount: completedComponents.length,
|
|
220
|
+
componentAtStep: templateWithReviewAndVerification.components[validInitialStep]?.type
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
template: templateWithReviewAndVerification,
|
|
226
|
+
currentComponentIndex: validInitialStep,
|
|
227
|
+
completedComponents: completedComponents,
|
|
228
|
+
componentData: {},
|
|
229
|
+
errors: {},
|
|
147
230
|
isProcessing: false,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
231
|
+
currentLanguage: initialLanguage,
|
|
232
|
+
showCustomStepper: true,
|
|
233
|
+
session: {
|
|
234
|
+
session_id: existingSessionId || '',
|
|
235
|
+
token: '',
|
|
236
|
+
isInitialized: false,
|
|
237
|
+
isProcessing: false,
|
|
238
|
+
error: null,
|
|
239
|
+
},
|
|
240
|
+
verification: {
|
|
241
|
+
status: 'idle',
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
};
|
|
154
245
|
|
|
155
246
|
// État du flux
|
|
156
247
|
const [state, setState] = useState<TemplateState>(() => buildInitialState());
|
|
248
|
+
|
|
249
|
+
// Fonction utilitaire pour convertir base64 en data URI pour l'affichage
|
|
250
|
+
const base64ToDataUri = useCallback((base64: string, mimeType: string = 'image/jpeg'): string => {
|
|
251
|
+
// Si c'est déjà une data URI, retourner tel quel
|
|
252
|
+
if (base64.startsWith('data:')) {
|
|
253
|
+
return base64;
|
|
254
|
+
}
|
|
255
|
+
// Sinon, créer une data URI
|
|
256
|
+
return `data:${mimeType};base64,${base64}`;
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
// Charger les données de session si on reprend une session existante
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
const loadSessionData = async () => {
|
|
262
|
+
// Ne charger que si on a une session existante
|
|
263
|
+
if (!existingSessionId) {
|
|
264
|
+
logger.log('No existingSessionId, skipping data load');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Si initialStep n'est pas défini ou est 0, on ne charge pas (début de session)
|
|
269
|
+
if (initialStep === undefined || initialStep === 0) {
|
|
270
|
+
logger.log('initialStep is 0 or undefined, skipping data load');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Attendre que la session soit initialisée
|
|
275
|
+
if (!state.session.isInitialized || !state.session.session_id) {
|
|
276
|
+
logger.log('Session not initialized yet, waiting...', {
|
|
277
|
+
isInitialized: state.session.isInitialized,
|
|
278
|
+
sessionId: state.session.session_id
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
logger.log('Loading session data for resume:', { sessionId: existingSessionId, step: initialStep });
|
|
285
|
+
const result = await kycService.getVerificationResult(state.session.session_id);
|
|
286
|
+
const sessionData = result[state.session.session_id]?.data;
|
|
287
|
+
|
|
288
|
+
if (sessionData) {
|
|
289
|
+
// Restaurer les données des composants depuis la session
|
|
290
|
+
// Utiliser 'as any' car VerificationResult peut avoir des propriétés dynamiques
|
|
291
|
+
const data: any = sessionData;
|
|
292
|
+
const restoredComponentData: Record<number, any> = {};
|
|
293
|
+
|
|
294
|
+
// Parcourir les composants jusqu'à l'étape initiale (incluse) pour restaurer leurs données
|
|
295
|
+
// Utiliser initialStep + 1 pour inclure le composant à l'étape initialStep
|
|
296
|
+
templateWithReviewAndVerification.components
|
|
297
|
+
.slice(0, initialStep + 1)
|
|
298
|
+
.forEach((component) => {
|
|
299
|
+
// Essayer de restaurer les données selon le type de composant
|
|
300
|
+
if (component.type === 'id_card' || component.type === 'file_upload') {
|
|
301
|
+
// Les documents peuvent être dans différentes structures
|
|
302
|
+
let documents: any = null;
|
|
303
|
+
|
|
304
|
+
// Chercher dans différentes structures possibles
|
|
305
|
+
if (data.documents) {
|
|
306
|
+
documents = data.documents;
|
|
307
|
+
} else if (data.user_data?.documents) {
|
|
308
|
+
documents = data.user_data.documents;
|
|
309
|
+
} else if (data.document_images) {
|
|
310
|
+
documents = data.document_images;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (documents) {
|
|
314
|
+
// Convertir les images base64 en format utilisable
|
|
315
|
+
const restoredDocuments: Record<string, any> = {};
|
|
316
|
+
|
|
317
|
+
Object.keys(documents).forEach((key) => {
|
|
318
|
+
const doc = documents[key];
|
|
319
|
+
if (typeof doc === 'object' && doc !== null) {
|
|
320
|
+
// Si on a un fichier base64, créer une structure avec dir et file
|
|
321
|
+
if (doc.file || doc.base64) {
|
|
322
|
+
const base64Data = doc.file || doc.base64;
|
|
323
|
+
restoredDocuments[key] = {
|
|
324
|
+
dir: base64ToDataUri(base64Data), // Utiliser data URI pour l'affichage
|
|
325
|
+
file: base64Data, // Garder le base64 pour l'envoi
|
|
326
|
+
mrz: doc.mrz || '',
|
|
327
|
+
templatePath: doc.templatePath || '',
|
|
328
|
+
};
|
|
329
|
+
} else {
|
|
330
|
+
// Sinon, garder la structure originale
|
|
331
|
+
restoredDocuments[key] = doc;
|
|
332
|
+
}
|
|
333
|
+
} else if (typeof doc === 'string') {
|
|
334
|
+
// Si c'est directement une string base64
|
|
335
|
+
restoredDocuments[key] = {
|
|
336
|
+
dir: base64ToDataUri(doc),
|
|
337
|
+
file: doc,
|
|
338
|
+
mrz: '',
|
|
339
|
+
templatePath: '',
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (Object.keys(restoredDocuments).length > 0) {
|
|
345
|
+
restoredComponentData[component.id] = restoredDocuments;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} else if (component.type === 'selfie') {
|
|
349
|
+
// Les selfies peuvent être dans sessionData.selfie_info
|
|
350
|
+
if (data.selfie_info) {
|
|
351
|
+
const selfieData: Record<string, any> = {};
|
|
352
|
+
const selfieInfo: any = data.selfie_info;
|
|
353
|
+
|
|
354
|
+
// Si selfie_info contient une image
|
|
355
|
+
if (selfieInfo.image) {
|
|
356
|
+
const base64Image = selfieInfo.image;
|
|
357
|
+
selfieData['front'] = {
|
|
358
|
+
dir: base64ToDataUri(base64Image),
|
|
359
|
+
file: base64Image,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Si on a plusieurs orientations
|
|
364
|
+
if (selfieInfo.orientations) {
|
|
365
|
+
Object.keys(selfieInfo.orientations).forEach((orientation) => {
|
|
366
|
+
const img = selfieInfo.orientations[orientation];
|
|
367
|
+
if (img) {
|
|
368
|
+
selfieData[orientation] = {
|
|
369
|
+
dir: base64ToDataUri(img),
|
|
370
|
+
file: img,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (Object.keys(selfieData).length > 0) {
|
|
377
|
+
restoredComponentData[component.id] = selfieData;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
} else if (component.type === 'country_selection') {
|
|
381
|
+
// Les données de sélection de pays peuvent être dans metadata
|
|
382
|
+
if (data.metadata || data.user_data) {
|
|
383
|
+
restoredComponentData[component.id] = {
|
|
384
|
+
...(data.metadata || {}),
|
|
385
|
+
...(data.user_data || {}),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
} else if (component.type === 'location') {
|
|
389
|
+
// Les données de localisation peuvent être dans metadata
|
|
390
|
+
if (data.metadata?.location || data.user_data?.location) {
|
|
391
|
+
restoredComponentData[component.id] = {
|
|
392
|
+
...(data.metadata?.location || {}),
|
|
393
|
+
...(data.user_data?.location || {}),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Mettre à jour l'état avec les données restaurées
|
|
400
|
+
if (Object.keys(restoredComponentData).length > 0) {
|
|
401
|
+
logger.log('Session data restored - components:', Object.keys(restoredComponentData));
|
|
402
|
+
logger.log('Session data restored - sample data:', truncateFields(restoredComponentData));
|
|
403
|
+
setState(prev => ({
|
|
404
|
+
...prev,
|
|
405
|
+
componentData: {
|
|
406
|
+
...prev.componentData,
|
|
407
|
+
...restoredComponentData,
|
|
408
|
+
},
|
|
409
|
+
}));
|
|
410
|
+
logger.log('Component data updated in state');
|
|
411
|
+
} else {
|
|
412
|
+
logger.log('No component data to restore from session');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} catch (error) {
|
|
416
|
+
logger.error('Error loading session data:', truncateFields(error));
|
|
417
|
+
// Ne pas bloquer le flux si le chargement échoue
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
loadSessionData();
|
|
422
|
+
}, [existingSessionId, initialStep, state.session.isInitialized, state.session.session_id, templateWithReviewAndVerification.components, base64ToDataUri]);
|
|
423
|
+
|
|
157
424
|
const mapComponentTypeToAction = useCallback((type: TemplateComponent['type']): string | null => {
|
|
158
425
|
switch (type) {
|
|
159
426
|
case 'id_card':
|
|
@@ -168,6 +435,9 @@ export const useTemplateKYCFlow = (
|
|
|
168
435
|
case 'country_selection':
|
|
169
436
|
// No direct backend action; pack into metadata of next actionable step
|
|
170
437
|
return null;
|
|
438
|
+
case 'welcome':
|
|
439
|
+
// UI-only step, no backend action needed
|
|
440
|
+
return null;
|
|
171
441
|
case 'initialization':
|
|
172
442
|
return 'initialize_session';
|
|
173
443
|
case 'verification_progress':
|
|
@@ -201,7 +471,7 @@ export const useTemplateKYCFlow = (
|
|
|
201
471
|
const buildPayloadForComponent = useCallback((action: string | null, component: TemplateComponent, rawData: any, templateId: string, step: number) => {
|
|
202
472
|
console.log('apiKey in buildPayloadForComponent', apiKey);
|
|
203
473
|
|
|
204
|
-
const base = { template_id: null, step: component.order, permissionGranted: true } as any;
|
|
474
|
+
const base = { template_id: templateId || null, templateId: templateId || null, step: component.order, permissionGranted: true } as any;
|
|
205
475
|
if (!action) {
|
|
206
476
|
return base;
|
|
207
477
|
}
|
|
@@ -230,9 +500,12 @@ export const useTemplateKYCFlow = (
|
|
|
230
500
|
const idCardID = Object.keys(state.componentData).find((c: string) => c === "1");
|
|
231
501
|
if (idCardID) {
|
|
232
502
|
const _idCardData = state.componentData[idCardID];
|
|
233
|
-
|
|
503
|
+
const documentType = _idCardData?.documentType;
|
|
504
|
+
// Map national_id to identity_card for selfie capture
|
|
505
|
+
const mappedDocumentType = documentType === 'national_id' ? 'identity_card' : (documentType as GovernmentDocumentType || 'identity_card');
|
|
506
|
+
return { ...base, documents, country: _idCardData?.country || '', documentType: mappedDocumentType };
|
|
234
507
|
}
|
|
235
|
-
|
|
508
|
+
return { ...base, documents };
|
|
236
509
|
}
|
|
237
510
|
|
|
238
511
|
if (action === 'location_permission') {
|
|
@@ -295,7 +568,18 @@ export const useTemplateKYCFlow = (
|
|
|
295
568
|
|
|
296
569
|
const token = apiKey ? undefined : await authentification();
|
|
297
570
|
console.log('token in initializeSession', { token, apiKey },);
|
|
298
|
-
|
|
571
|
+
|
|
572
|
+
// Check if we already have a session ID from URL params (passed via state or prop)
|
|
573
|
+
let session;
|
|
574
|
+
const existingSessionId = state.session.session_id; // This might be set from initial props if we add logic for it
|
|
575
|
+
|
|
576
|
+
if (existingSessionId && existingSessionId.length > 0) {
|
|
577
|
+
logger.log('Resuming existing session:', existingSessionId);
|
|
578
|
+
// Verify existence/validity if needed, for now trust the ID and just fetch/use it
|
|
579
|
+
session = { session_id: existingSessionId };
|
|
580
|
+
} else {
|
|
581
|
+
session = await kycService.newSession({ token, apiKey });
|
|
582
|
+
}
|
|
299
583
|
|
|
300
584
|
// Align backend flow from step 0 with initialize_session
|
|
301
585
|
try {
|
|
@@ -371,6 +655,24 @@ export const useTemplateKYCFlow = (
|
|
|
371
655
|
|
|
372
656
|
return componentData && componentData.code && componentData.regionMapping;
|
|
373
657
|
|
|
658
|
+
case 'welcome':
|
|
659
|
+
// Welcome is valid once user has given consent (componentData is set when they click Get Started)
|
|
660
|
+
return componentData && componentData.consentGiven !== false;
|
|
661
|
+
|
|
662
|
+
case 'email_verification':
|
|
663
|
+
return componentData && componentData.verified === true;
|
|
664
|
+
|
|
665
|
+
case 'phone_verification':
|
|
666
|
+
return componentData && componentData.verified === true;
|
|
667
|
+
|
|
668
|
+
case 'personal_information':
|
|
669
|
+
return componentData && Object.keys(componentData).length > 0;
|
|
670
|
+
|
|
671
|
+
case 'additional_documents':
|
|
672
|
+
// Optional by default in template config, but if required we should check based on config
|
|
673
|
+
// For now, return true or check length if present
|
|
674
|
+
return true;
|
|
675
|
+
|
|
374
676
|
case 'review_submit':
|
|
375
677
|
return true;
|
|
376
678
|
default:
|
|
@@ -403,10 +705,16 @@ export const useTemplateKYCFlow = (
|
|
|
403
705
|
const currentComp = state.template.components[state.currentComponentIndex];
|
|
404
706
|
if (!currentComp) return;
|
|
405
707
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
isProcessing
|
|
409
|
-
|
|
708
|
+
// Prevent multiple simultaneous calls
|
|
709
|
+
setState(prev => {
|
|
710
|
+
if (prev.isProcessing) {
|
|
711
|
+
return prev;
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
...prev,
|
|
715
|
+
isProcessing: true,
|
|
716
|
+
};
|
|
717
|
+
});
|
|
410
718
|
// Valider le composant actuel
|
|
411
719
|
if (!validateComponent(currentComp.id)) {
|
|
412
720
|
setState(prev => ({
|
|
@@ -423,7 +731,13 @@ export const useTemplateKYCFlow = (
|
|
|
423
731
|
|
|
424
732
|
try {
|
|
425
733
|
const component = state.template.components.find(c => c.id === currentComp.id);
|
|
426
|
-
if (!component)
|
|
734
|
+
if (!component) {
|
|
735
|
+
setState(prev => ({
|
|
736
|
+
...prev,
|
|
737
|
+
isProcessing: false,
|
|
738
|
+
}));
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
427
741
|
if (component.type === 'review_submit') {
|
|
428
742
|
// Move to verification screen and mark verification in progress
|
|
429
743
|
setState(prev => ({
|
|
@@ -484,7 +798,7 @@ export const useTemplateKYCFlow = (
|
|
|
484
798
|
session_id: state.session.session_id,
|
|
485
799
|
step: step,
|
|
486
800
|
data: payloadData,
|
|
487
|
-
templateId:
|
|
801
|
+
templateId: templateId,
|
|
488
802
|
token: state.session.token,
|
|
489
803
|
action: action,
|
|
490
804
|
apiKey: apiKey ?? "-",
|
|
@@ -504,7 +818,7 @@ export const useTemplateKYCFlow = (
|
|
|
504
818
|
}));
|
|
505
819
|
|
|
506
820
|
} catch (error) {
|
|
507
|
-
|
|
821
|
+
logger.error('Error in nextComponent:', error);
|
|
508
822
|
setState(prev => ({
|
|
509
823
|
...prev,
|
|
510
824
|
isProcessing: false,
|
|
@@ -515,7 +829,7 @@ export const useTemplateKYCFlow = (
|
|
|
515
829
|
}));
|
|
516
830
|
}
|
|
517
831
|
|
|
518
|
-
}, [canGoNext, state.currentComponentIndex, state.template.components, validateComponent, apiKey, state.session.session_id, state.session.token]),
|
|
832
|
+
}, [canGoNext, state.currentComponentIndex, state.template.components, validateComponent, apiKey, state.session.session_id, state.session.token, buildPayloadForComponent, mapComponentTypeToAction, chooseTemplateId, state.currentLanguage]),
|
|
519
833
|
|
|
520
834
|
// Retourner au composant précédent
|
|
521
835
|
previousComponent: useCallback(() => {
|
|
@@ -638,5 +952,6 @@ export const useTemplateKYCFlow = (
|
|
|
638
952
|
isComplete,
|
|
639
953
|
getLocalizedText,
|
|
640
954
|
initializeSession,
|
|
955
|
+
env,
|
|
641
956
|
};
|
|
642
957
|
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from 'react';
|
|
2
|
+
import { KYCTemplate } from '../types/KYC.types';
|
|
3
|
+
import templateService from '../modules/api/TemplateService';
|
|
4
|
+
import { transformBackendTemplateToSDK, validateTransformedTemplate } from '../utils/template-transformer';
|
|
5
|
+
import { logger } from '../utils/logger';
|
|
6
|
+
import { authentification } from '../modules/api/KYCService';
|
|
7
|
+
|
|
8
|
+
export interface UseTemplateLoaderReturn {
|
|
9
|
+
template: KYCTemplate | null;
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
error: string | null;
|
|
12
|
+
loadTemplate: (templateId: string, apiKey?: string) => Promise<void>;
|
|
13
|
+
refresh: () => void;
|
|
14
|
+
clearError: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Hook to load and transform templates from the backend API
|
|
19
|
+
*/
|
|
20
|
+
export function useTemplateLoader(): UseTemplateLoaderReturn {
|
|
21
|
+
const [template, setTemplate] = useState<KYCTemplate | null>(null);
|
|
22
|
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
23
|
+
const [error, setError] = useState<string | null>(null);
|
|
24
|
+
const currentTemplateIdRef = useRef<string | null>(null);
|
|
25
|
+
const currentApiKeyRef = useRef<string | undefined>(undefined);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load a template by ID
|
|
29
|
+
*/
|
|
30
|
+
const loadTemplate = useCallback(async (templateId: string, apiKey?: string) => {
|
|
31
|
+
// Reset state
|
|
32
|
+
setError(null);
|
|
33
|
+
setIsLoading(true);
|
|
34
|
+
currentTemplateIdRef.current = templateId;
|
|
35
|
+
currentApiKeyRef.current = apiKey;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
logger.log(`Loading template: ${templateId}`);
|
|
39
|
+
|
|
40
|
+
// Get token if no API key provided
|
|
41
|
+
let token: string | undefined;
|
|
42
|
+
if (!apiKey) {
|
|
43
|
+
try {
|
|
44
|
+
token = await authentification();
|
|
45
|
+
} catch (authError) {
|
|
46
|
+
logger.error('Authentication failed:', authError);
|
|
47
|
+
throw new Error('Failed to authenticate. Please provide a valid API key.');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Fetch template from backend
|
|
52
|
+
const backendTemplate = await templateService.fetchTemplate(templateId, apiKey, token);
|
|
53
|
+
|
|
54
|
+
// Transform to SDK format
|
|
55
|
+
const transformedTemplate = transformBackendTemplateToSDK(backendTemplate);
|
|
56
|
+
|
|
57
|
+
// Validate transformed template
|
|
58
|
+
if (!validateTransformedTemplate(transformedTemplate)) {
|
|
59
|
+
throw new Error('Transformed template validation failed');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Set the template
|
|
63
|
+
setTemplate(transformedTemplate);
|
|
64
|
+
logger.log(`Template loaded successfully: ${templateId}`);
|
|
65
|
+
} catch (err: any) {
|
|
66
|
+
const errorMessage = err.message || 'Failed to load template';
|
|
67
|
+
logger.error(`Error loading template ${templateId}:`, errorMessage);
|
|
68
|
+
setError(errorMessage);
|
|
69
|
+
setTemplate(null);
|
|
70
|
+
} finally {
|
|
71
|
+
setIsLoading(false);
|
|
72
|
+
}
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Refresh the current template
|
|
77
|
+
*/
|
|
78
|
+
const refresh = useCallback(() => {
|
|
79
|
+
if (currentTemplateIdRef.current) {
|
|
80
|
+
// Clear cache for this template
|
|
81
|
+
templateService.clearCache(currentTemplateIdRef.current);
|
|
82
|
+
// Reload template
|
|
83
|
+
loadTemplate(currentTemplateIdRef.current, currentApiKeyRef.current);
|
|
84
|
+
}
|
|
85
|
+
}, [loadTemplate]);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Clear error state
|
|
89
|
+
*/
|
|
90
|
+
const clearError = useCallback(() => {
|
|
91
|
+
setError(null);
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
template,
|
|
96
|
+
isLoading,
|
|
97
|
+
error,
|
|
98
|
+
loadTemplate,
|
|
99
|
+
refresh,
|
|
100
|
+
clearError,
|
|
101
|
+
};
|
|
102
|
+
}
|