@myazahq/kyc-sdk-react-native 2.0.2 → 2.2.0
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/README.md +152 -8
- package/android/build.gradle +6 -0
- package/android/src/main/AndroidManifest.xml +14 -4
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaEmrtd.kt +358 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +88 -1
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaRectDetector.kt +34 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +118 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/Jp2Decoder.kt +117 -0
- package/app.plugin.js +102 -13
- package/ios/HybridMyazaEmrtd.swift +447 -0
- package/ios/HybridMyazaFaceDetector.swift +104 -2
- package/ios/HybridMyazaRectDetector.swift +103 -0
- package/ios/HybridMyazaTextRecognizer.swift +111 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/KycSdkReactNative+autolinking.cmake +87 -0
- package/nitrogen/generated/android/KycSdkReactNative+autolinking.gradle +27 -0
- package/nitrogen/generated/android/KycSdkReactNativeOnLoad.cpp +102 -0
- package/nitrogen/generated/android/KycSdkReactNativeOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JDetectedRect.hpp +81 -0
- package/nitrogen/generated/android/c++/JEmrtdApduResponse.hpp +61 -0
- package/nitrogen/generated/android/c++/JEmrtdTagInfo.hpp +61 -0
- package/nitrogen/generated/android/c++/JFaceResult.hpp +113 -0
- package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.cpp +176 -0
- package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +60 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +63 -0
- package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.cpp +60 -0
- package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.hpp +63 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +80 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +64 -0
- package/nitrogen/generated/android/c++/JTextResult.hpp +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/DetectedRect.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdApduResponse.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdTagInfo.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/FaceResult.kt +121 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaEmrtdSpec.kt +103 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +55 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaRectDetectorSpec.kt +55 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +60 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/KycSdkReactNativeOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/TextResult.kt +51 -0
- package/nitrogen/generated/ios/KycSdkReactNative+autolinking.rb +62 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +143 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +423 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Umbrella.hpp +81 -0
- package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.mm +57 -0
- package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.swift +62 -0
- package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.hpp +184 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +87 -0
- package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.hpp +87 -0
- package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +98 -0
- package/nitrogen/generated/ios/swift/DetectedRect.swift +59 -0
- package/nitrogen/generated/ios/swift/EmrtdApduResponse.swift +34 -0
- package/nitrogen/generated/ios/swift/EmrtdTagInfo.swift +34 -0
- package/nitrogen/generated/ios/swift/FaceResult.swift +99 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_EmrtdApduResponse.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_EmrtdTagInfo.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_TextResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec.swift +67 -0
- package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec_cxx.swift +317 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +143 -0
- package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec_cxx.swift +143 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +57 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +162 -0
- package/nitrogen/generated/ios/swift/TextResult.swift +35 -0
- package/nitrogen/generated/shared/c++/DetectedRect.hpp +107 -0
- package/nitrogen/generated/shared/c++/EmrtdApduResponse.hpp +87 -0
- package/nitrogen/generated/shared/c++/EmrtdTagInfo.hpp +87 -0
- package/nitrogen/generated/shared/c++/FaceResult.hpp +139 -0
- package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.cpp +33 -0
- package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.hpp +80 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +67 -0
- package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.hpp +67 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +70 -0
- package/nitrogen/generated/shared/c++/TextResult.hpp +84 -0
- package/package.json +16 -4
- package/src/MyazaKYC.tsx +115 -22
- package/src/capture/documentIdentity.ts +153 -0
- package/src/capture/documentSignals.ts +177 -0
- package/src/capture/documentTextGate.ts +171 -0
- package/src/capture/hints.ts +57 -0
- package/src/capture/index.ts +24 -0
- package/src/capture/rectDetector.ts +84 -0
- package/src/capture/rectGate.ts +91 -0
- package/src/capture/useAutoCapture.ts +198 -0
- package/src/components/CameraViewfinder.tsx +226 -70
- package/src/components/ContactCodeEntry.tsx +139 -0
- package/src/components/CountryField.tsx +87 -0
- package/src/components/CountryFlag.tsx +15 -15
- package/src/components/DialCodePicker.tsx +186 -0
- package/src/components/DocumentCropper.tsx +1 -1
- package/src/components/DocumentReview.tsx +142 -0
- package/src/components/DocumentReviewSide.tsx +135 -0
- package/src/components/DocumentReviewZoom.tsx +119 -0
- package/src/components/ExpiryCountdown.tsx +52 -0
- package/src/components/FlashOverlay.tsx +156 -0
- package/src/components/Icon.tsx +70 -1
- package/src/components/KycFlow.tsx +66 -94
- package/src/components/KycSheet.tsx +148 -18
- package/src/components/MediaSourceSheet.tsx +163 -0
- package/src/components/MyazaButton.tsx +14 -2
- package/src/components/MyazaDateField.tsx +240 -0
- package/src/components/MyazaInput.tsx +73 -26
- package/src/components/MyazaSelect.tsx +283 -0
- package/src/components/MyazaWordmark.tsx +33 -0
- package/src/components/OptionRow.tsx +132 -0
- package/src/components/PhoneNumberInput.tsx +149 -0
- package/src/components/PoweredBy.tsx +89 -0
- package/src/components/ReadyPrimer.tsx +149 -0
- package/src/components/StepView.tsx +91 -0
- package/src/components/Typography.tsx +18 -3
- package/src/components/VerifiedNotice.tsx +38 -0
- package/src/components/WhatsAppIcon.tsx +29 -0
- package/src/components/WorkflowGate.tsx +128 -0
- package/src/components/brand-font.ts +113 -0
- package/src/components/flashHoleGeometry.ts +35 -0
- package/src/components/flow/FatalConfigError.tsx +56 -0
- package/src/components/fonts.ts +2 -19
- package/src/components/readyPrimerContent.ts +52 -0
- package/src/components/runtime.tsx +38 -43
- package/src/components/stepHeaderMeta.tsx +87 -0
- package/src/components/theme-provider.tsx +89 -0
- package/src/components/useWorkflowMount.ts +116 -0
- package/src/components/viewfinder/CardGuide.tsx +60 -0
- package/src/components/viewfinder/DocumentGhost.tsx +149 -0
- package/src/components/viewfinder/ImmersiveBottomBar.tsx +172 -0
- package/src/components/viewfinder/ImmersiveControls.tsx +127 -0
- package/src/components/viewfinder/ImmersiveGuide.tsx +174 -0
- package/src/components/viewfinder/ImmersiveOverlay.tsx +79 -0
- package/src/components/viewfinder/ViewfinderControls.tsx +154 -0
- package/src/config/brand.ts +97 -0
- package/src/config/business.ts +179 -0
- package/src/config/businessSteps.ts +105 -0
- package/src/config/contact.ts +159 -0
- package/src/config/countryNames.g.ts +254 -0
- package/src/config/currencyFlags.ts +144 -0
- package/src/config/font-resolve.ts +78 -0
- package/src/config/idTypes.ts +96 -1
- package/src/config/keyPeople.ts +162 -0
- package/src/config/phone.ts +39 -0
- package/src/config/proofOfAddress.ts +75 -0
- package/src/config/questionnaire.ts +153 -0
- package/src/config/regions.ts +118 -0
- package/src/config/registrationHint.ts +87 -0
- package/src/config/stepOrder.ts +118 -0
- package/src/config/theme.ts +38 -2
- package/src/config/workflowMerge.ts +124 -0
- package/src/emrtd/bac.ts +128 -0
- package/src/emrtd/bytes.ts +127 -0
- package/src/emrtd/crypto.ts +163 -0
- package/src/emrtd/der.ts +192 -0
- package/src/emrtd/dg2.ts +111 -0
- package/src/emrtd/files.ts +215 -0
- package/src/emrtd/index.ts +30 -0
- package/src/emrtd/mrzKey.ts +54 -0
- package/src/emrtd/native.ts +79 -0
- package/src/emrtd/optionalRead.ts +42 -0
- package/src/emrtd/read.ts +224 -0
- package/src/emrtd/retry.ts +98 -0
- package/src/emrtd/secureMessaging.ts +178 -0
- package/src/emrtd/session.ts +200 -0
- package/src/emrtd/stages.ts +122 -0
- package/src/index.ts +110 -0
- package/src/lib/calendar.ts +71 -0
- package/src/lib/use-keyboard-inset.ts +34 -0
- package/src/liveness/challengeManager.ts +27 -1
- package/src/liveness/faceContinuity.ts +153 -0
- package/src/liveness/flashDetector.ts +145 -0
- package/src/liveness/flashReadyGate.ts +106 -0
- package/src/liveness/flashRunner.ts +118 -0
- package/src/liveness/integritySignals.ts +60 -0
- package/src/liveness/livenessState.ts +139 -0
- package/src/liveness/speech.ts +19 -1
- package/src/liveness/types.ts +48 -1
- package/src/liveness/useFlashPhase.ts +78 -0
- package/src/liveness/useLiveness.ts +260 -75
- package/src/liveness/visionCameraFaceDetector.ts +14 -0
- package/src/mrz/extract.ts +101 -0
- package/src/mrz/parse.ts +178 -0
- package/src/mrz/textRecognizer.ts +149 -0
- package/src/screens/ApplicantRoleStep.tsx +322 -0
- package/src/screens/BusinessDetailsStep.tsx +212 -0
- package/src/screens/BusinessDocumentSlot.tsx +167 -0
- package/src/screens/BusinessDocumentsStep.tsx +213 -0
- package/src/screens/BusinessKeyPeopleStep.tsx +207 -0
- package/src/screens/CompanyInfoFields.tsx +83 -0
- package/src/screens/ConsentStep.tsx +68 -68
- package/src/screens/ContactChannelChoice.tsx +133 -0
- package/src/screens/ContactCodeStep.tsx +130 -0
- package/src/screens/ContactDestinationField.tsx +70 -0
- package/src/screens/ContactFooterNote.tsx +34 -0
- package/src/screens/ContactVerificationStep.tsx +199 -0
- package/src/screens/ContactVerifiedPanel.tsx +36 -0
- package/src/screens/CountrySelectStep.tsx +210 -0
- package/src/screens/DocumentCaptureStep.tsx +156 -132
- package/src/screens/IdInputStep.tsx +13 -5
- package/src/screens/IdTypeStep.tsx +74 -33
- package/src/screens/KeyPeopleInviteLinks.tsx +239 -0
- package/src/screens/KeyPersonCard.tsx +119 -0
- package/src/screens/KeyPersonForm.tsx +148 -0
- package/src/screens/KeyPersonSheet.tsx +184 -0
- package/src/screens/LivenessStep.tsx +155 -247
- package/src/screens/MrzScanView.tsx +173 -0
- package/src/screens/NfcStep.tsx +290 -0
- package/src/screens/ProofOfAddressStep.tsx +238 -0
- package/src/screens/QuestionnaireField.tsx +219 -0
- package/src/screens/QuestionnaireMoneyField.tsx +134 -0
- package/src/screens/QuestionnaireStep.tsx +76 -0
- package/src/screens/SubmittedStep.tsx +5 -0
- package/src/screens/consent/model.ts +109 -0
- package/src/screens/document/CameraPhase.tsx +150 -0
- package/src/screens/document/RequiredPill.tsx +60 -0
- package/src/screens/liveness/LightingBanner.tsx +50 -0
- package/src/screens/liveness/LivenessOutcome.tsx +109 -0
- package/src/screens/liveness/ProgressDots.tsx +67 -0
- package/src/screens/liveness/SelfiePreview.tsx +39 -0
- package/src/screens/liveness/constants.ts +31 -0
- package/src/screens/liveness/index.ts +8 -0
- package/src/screens/liveness/resolveGuidance.ts +27 -0
- package/src/screens/liveness/useSelfieUpload.ts +107 -0
- package/src/screens/nfc/NfcMrzPrompt.tsx +40 -0
- package/src/screens/nfc/NfcReadBody.tsx +63 -0
- package/src/screens/nfc/NfcReadProgress.tsx +96 -0
- package/src/screens/nfc/NfcScanIllustration.tsx +244 -0
- package/src/screens/nfc/NfcScannedSummary.tsx +74 -0
- package/src/screens/nfc/NfcSuccessPanel.tsx +162 -0
- package/src/screens/nfc/availability.ts +73 -0
- package/src/screens/nfc/readErrorMessage.ts +93 -0
- package/src/screens/nfc/useChipPortrait.ts +58 -0
- package/src/screens/nfc/useNfcAvailability.ts +82 -0
- package/src/screens/useFlashHole.ts +105 -0
- package/src/screens/useFlashSequence.ts +96 -0
- package/src/screens/usePoaAttach.ts +121 -0
- package/src/services/api-types.ts +164 -0
- package/src/services/api-verify-types.ts +99 -0
- package/src/services/api.ts +72 -102
- package/src/services/cardCrop.ts +25 -10
- package/src/services/contactErrors.ts +43 -0
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/documentPicker.ts +20 -0
- package/src/services/fingerprint-sources.ts +129 -0
- package/src/services/fingerprint.ts +101 -0
- package/src/services/haptics.ts +38 -0
- package/src/services/mediaCompress.ts +7 -2
- package/src/services/resolveUrl.ts +2 -2
- package/src/services/workflowGate.ts +138 -0
- package/src/specs/MyazaEmrtd.nitro.ts +114 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +10 -0
- package/src/specs/MyazaRectDetector.nitro.ts +50 -0
- package/src/specs/MyazaTextRecognizer.nitro.ts +48 -0
- package/src/store/derive.ts +225 -0
- package/src/store/kycStore.ts +250 -149
- package/src/store/state.ts +253 -0
- package/src/store/submit.ts +101 -0
- package/src/store/submitApplicant.ts +81 -0
- package/src/types/appearance.ts +113 -0
- package/src/types/business.ts +131 -0
- package/src/types/config.ts +150 -124
- package/src/types/id-types.ts +57 -0
- package/src/types/verification.ts +5 -0
- package/src/types/workflow.ts +141 -0
- package/src/utils/amountFormat.ts +73 -0
- package/src/utils/tokens.ts +4 -2
- package/src/__tests__/cardCrop.test.ts +0 -39
- package/src/__tests__/deviceMetadata.test.ts +0 -34
- package/src/__tests__/errors.test.ts +0 -34
- package/src/__tests__/flow.test.ts +0 -61
- package/src/__tests__/gestureDetector.test.ts +0 -37
- package/src/__tests__/liveness.test.ts +0 -112
- package/src/__tests__/resolveUrl.test.ts +0 -64
- package/src/__tests__/validators.test.ts +0 -38
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { Image, Pressable, View } from 'react-native';
|
|
2
|
+
import { Image, Platform, Pressable, View } from 'react-native';
|
|
3
3
|
import * as ImagePicker from 'expo-image-picker';
|
|
4
4
|
import { useCameraDevice, useCameraPermission } from 'react-native-vision-camera';
|
|
5
|
+
import { initialWindowMetrics } from 'react-native-safe-area-context';
|
|
5
6
|
|
|
6
7
|
import { radius, spacing } from '../config/theme';
|
|
7
8
|
import { ID_TYPES, documentGuideAspect, getScanSides } from '../config/idTypes';
|
|
@@ -11,14 +12,18 @@ import { compressDocumentImage, compressVideo, cropCardRegion } from '../service
|
|
|
11
12
|
import { MAX_VIDEO_BYTES } from '../config/captureSettings';
|
|
12
13
|
import { KYCError } from '../types/verification';
|
|
13
14
|
import type { DocumentCapturePhase } from '../store/kycStore';
|
|
14
|
-
import { useKyc, useKycConfig, useTheme } from '../components/runtime';
|
|
15
|
+
import { useEffectiveCountry, useKyc, useKycConfig, useKycStore, useTheme } from '../components/runtime';
|
|
15
16
|
import { MyazaText } from '../components/Typography';
|
|
16
17
|
import { MyazaButton } from '../components/MyazaButton';
|
|
17
18
|
import { useToast } from '../components/toast';
|
|
18
19
|
import { MyazaPulseLoader } from '../components/MyazaPulseLoader';
|
|
19
|
-
import {
|
|
20
|
+
import { CameraPhase } from './document/CameraPhase';
|
|
20
21
|
import { CameraPermissionView, CameraUnavailableView, CameraPermissionPrimingView } from '../components/CameraPermissionView';
|
|
22
|
+
import { ReadyPrimer } from '../components/ReadyPrimer';
|
|
23
|
+
import { READY_DOCUMENT } from '../components/readyPrimerContent';
|
|
21
24
|
import { DocumentCropper } from '../components/DocumentCropper';
|
|
25
|
+
import { DocumentReview } from '../components/DocumentReview';
|
|
26
|
+
import { scanMrzFromImage } from '../mrz/textRecognizer';
|
|
22
27
|
import { Icon } from '../components/Icon';
|
|
23
28
|
|
|
24
29
|
// Document capture — the RN mirror of the Flutter/web DocumentCaptureStep.
|
|
@@ -61,6 +66,16 @@ export function documentCaptureMeta(
|
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
/** Bound on how long Continue waits for the supplementary document video. */
|
|
70
|
+
const DOC_VIDEO_WAIT_MS = 8000;
|
|
71
|
+
|
|
72
|
+
/** Home-indicator / nav-bar clearance for the full-screen camera's controls. */
|
|
73
|
+
function cameraBottomInset(): number {
|
|
74
|
+
const sa = initialWindowMetrics?.insets;
|
|
75
|
+
if (Platform.OS === 'android') return (sa?.bottom ?? 0) + 12;
|
|
76
|
+
return sa?.bottom || 34;
|
|
77
|
+
}
|
|
78
|
+
|
|
64
79
|
export function DocumentCaptureStep(): React.ReactElement {
|
|
65
80
|
const { colors } = useTheme();
|
|
66
81
|
const toast = useToast();
|
|
@@ -68,7 +83,12 @@ export function DocumentCaptureStep(): React.ReactElement {
|
|
|
68
83
|
const selectedIdType = useKyc((s) => s.selectedIdType);
|
|
69
84
|
const api = useKyc((s) => s.api);
|
|
70
85
|
const setMediaId = useKyc((s) => s.setMediaId);
|
|
86
|
+
const setMrzScan = useKyc((s) => s.setMrzScan);
|
|
87
|
+
const mrzScan = useKyc((s) => s.mrzScan);
|
|
88
|
+
const country = useEffectiveCountry();
|
|
71
89
|
const setDocumentCapturePhase = useKyc((s) => s.setDocumentCapturePhase);
|
|
90
|
+
const setImmersiveCapture = useKyc((s) => s.setImmersiveCapture);
|
|
91
|
+
const store = useKycStore();
|
|
72
92
|
const nextStep = useKyc((s) => s.nextStep);
|
|
73
93
|
|
|
74
94
|
const allowUpload = config.allowDocumentUpload !== false;
|
|
@@ -96,6 +116,7 @@ export function DocumentCaptureStep(): React.ReactElement {
|
|
|
96
116
|
setDocumentCapturePhase(phase);
|
|
97
117
|
}, [phase, setDocumentCapturePhase]);
|
|
98
118
|
|
|
119
|
+
|
|
99
120
|
// ── Camera permission ──────────────────────────────────────────────────────
|
|
100
121
|
// `perm` is derived from the ASYNC requestPermission result, not synchronously
|
|
101
122
|
// from `hasPermission` — otherwise the brief window while the OS prompt is open
|
|
@@ -127,7 +148,23 @@ export function DocumentCaptureStep(): React.ReactElement {
|
|
|
127
148
|
// No camera hardware at all → "Camera not available" (regardless of what the
|
|
128
149
|
// permission API says — on a camera-less sim it may even report denied).
|
|
129
150
|
const cameraUnavailable = cameraGrace && !device;
|
|
151
|
+
// Gate the camera path on the user acknowledging the primer. Shown once for
|
|
152
|
+
// the step, not per side — repeating it before the back of a card would be
|
|
153
|
+
// noise, not a warning.
|
|
154
|
+
const [ready, setReady] = useState(false);
|
|
130
155
|
const showPrimer = perm === 'priming' && !!device;
|
|
156
|
+
// Tell the shell when the full-bleed camera is on screen. Scoped tightly:
|
|
157
|
+
// only the live preview earns the whole display — the ready primer, the
|
|
158
|
+
// permission screens, the previews and the review all keep their chrome.
|
|
159
|
+
//
|
|
160
|
+
// No unmount cleanup: KycFlow already scopes the flag to this step, and a
|
|
161
|
+
// cleanup here would fire during any remount and fight the effect that had
|
|
162
|
+
// just raised it.
|
|
163
|
+
const cameraLive =
|
|
164
|
+
(phase === 'front' || phase === 'back') && ready && perm === 'granted' && !!device;
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
setImmersiveCapture(cameraLive);
|
|
167
|
+
}, [cameraLive, setImmersiveCapture]);
|
|
131
168
|
|
|
132
169
|
// Reflect an externally-granted permission.
|
|
133
170
|
useEffect(() => {
|
|
@@ -176,6 +213,22 @@ export function DocumentCaptureStep(): React.ReactElement {
|
|
|
176
213
|
setUploadError(null);
|
|
177
214
|
try {
|
|
178
215
|
const compressed = await compressDocumentImage(rawUri);
|
|
216
|
+
// Read the MRZ off the FRONT while we have it. It is the key that
|
|
217
|
+
// unlocks the chip, and taking it from the photo the user just captured
|
|
218
|
+
// is what spares them scanning the same passport a second time.
|
|
219
|
+
// Best-effort in every sense: most documents have no MRZ, and one that
|
|
220
|
+
// does not read simply leaves the chip step to ask.
|
|
221
|
+
if (phase !== 'back') {
|
|
222
|
+
void scanMrzFromImage(compressed)
|
|
223
|
+
.then((scan) => {
|
|
224
|
+
if (scan) setMrzScan(scan);
|
|
225
|
+
})
|
|
226
|
+
// Explicit, even though the scanner already swallows its own
|
|
227
|
+
// errors: this sits inside the capture path, and the claim above
|
|
228
|
+
// that it is best-effort has to hold for anything that can reject
|
|
229
|
+
// — including a future change to the store setter.
|
|
230
|
+
.catch(() => undefined);
|
|
231
|
+
}
|
|
179
232
|
if (phase === 'back') {
|
|
180
233
|
setBackUri(compressed);
|
|
181
234
|
setPhase('review');
|
|
@@ -187,7 +240,7 @@ export function DocumentCaptureStep(): React.ReactElement {
|
|
|
187
240
|
setBusy(false);
|
|
188
241
|
}
|
|
189
242
|
},
|
|
190
|
-
[phase, isTwoSided],
|
|
243
|
+
[phase, isTwoSided, setMrzScan],
|
|
191
244
|
);
|
|
192
245
|
|
|
193
246
|
// Gallery upload: pick the raw photo (no native editor), then open the SDK's
|
|
@@ -196,13 +249,16 @@ export function DocumentCaptureStep(): React.ReactElement {
|
|
|
196
249
|
// (mirrors Flutter's cropCardRegion), then store. Gallery photos skip this —
|
|
197
250
|
// they're already cropped by the interactive cropper.
|
|
198
251
|
const captureFromCamera = useCallback(
|
|
199
|
-
async (rawUri: string, videoPath: string | null) => {
|
|
252
|
+
async (rawUri: string, videoPath: string | null, viewAr: number) => {
|
|
200
253
|
setBusy(true);
|
|
201
254
|
try {
|
|
202
255
|
// Stash the side's video (best-effort) before cropping/storing the still.
|
|
203
256
|
if (phase === 'back') backVideoRef.current = videoPath;
|
|
204
257
|
else frontVideoRef.current = videoPath;
|
|
205
|
-
|
|
258
|
+
// viewAr is the MEASURED preview box — full-screen capture shows a far
|
|
259
|
+
// taller slice than the framed 3:4 box the old constant assumed, which
|
|
260
|
+
// is why reviews came back wider than what the user framed.
|
|
261
|
+
const carded = await cropCardRegion(rawUri, guideAspect, viewAr).catch(() => rawUri);
|
|
206
262
|
await storeCapture(carded);
|
|
207
263
|
} finally {
|
|
208
264
|
setBusy(false);
|
|
@@ -223,6 +279,18 @@ export function DocumentCaptureStep(): React.ReactElement {
|
|
|
223
279
|
}, []);
|
|
224
280
|
|
|
225
281
|
// Best-effort document video upload — never blocks or fails the flow.
|
|
282
|
+
/**
|
|
283
|
+
* How long the flow will WAIT for a document video before moving on.
|
|
284
|
+
*
|
|
285
|
+
* The clip is supplementary evidence, but transcoding a 4K recording and
|
|
286
|
+
* pushing it over a phone hotspot is slow — and it used to be awaited with no
|
|
287
|
+
* bound, so the step sat on a spinner indefinitely after the photo had
|
|
288
|
+
* already uploaded fine. The user could not reach the chip step at all.
|
|
289
|
+
*
|
|
290
|
+
* The upload is NOT cancelled at the deadline: it keeps running in the
|
|
291
|
+
* background and still records its media id if it lands, usually well before
|
|
292
|
+
* submission. Only the WAIT is bounded.
|
|
293
|
+
*/
|
|
226
294
|
const uploadDocVideo = useCallback(
|
|
227
295
|
async (
|
|
228
296
|
videoPath: string | null,
|
|
@@ -230,15 +298,21 @@ export function DocumentCaptureStep(): React.ReactElement {
|
|
|
230
298
|
mediaKey: 'documentFrontVideo' | 'documentBackVideo',
|
|
231
299
|
) => {
|
|
232
300
|
if (!videoPath) return;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
301
|
+
const work = (async () => {
|
|
302
|
+
try {
|
|
303
|
+
// Transcode the raw 4K recording down to a small evidence clip first.
|
|
304
|
+
const small = await compressVideo(videoPath);
|
|
305
|
+
const id = await withRetry(() => api.upload({ uri: small, type: 'video/mp4' }, type, MAX_VIDEO_BYTES));
|
|
306
|
+
setMediaId(mediaKey, id);
|
|
307
|
+
} catch {
|
|
308
|
+
/* supplementary — verification proceeds without the document video
|
|
309
|
+
(dropped if it failed to upload or exceeded the 5MB ceiling) */
|
|
310
|
+
}
|
|
311
|
+
})();
|
|
312
|
+
await Promise.race([
|
|
313
|
+
work,
|
|
314
|
+
new Promise<void>((resolve) => setTimeout(resolve, DOC_VIDEO_WAIT_MS)),
|
|
315
|
+
]);
|
|
242
316
|
},
|
|
243
317
|
[api, setMediaId],
|
|
244
318
|
);
|
|
@@ -312,6 +386,17 @@ export function DocumentCaptureStep(): React.ReactElement {
|
|
|
312
386
|
</>
|
|
313
387
|
);
|
|
314
388
|
}
|
|
389
|
+
if (!ready) {
|
|
390
|
+
// "Here's what happens next", BEFORE the OS prompt — and shown even when
|
|
391
|
+
// permission is already granted, since it is about what the step asks of
|
|
392
|
+
// the user rather than about access.
|
|
393
|
+
return (
|
|
394
|
+
<>
|
|
395
|
+
{cropper}
|
|
396
|
+
<ReadyPrimer content={READY_DOCUMENT} onReady={() => setReady(true)} />
|
|
397
|
+
</>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
315
400
|
if (showPrimer) {
|
|
316
401
|
// Primer before the OS prompt — camera not started yet.
|
|
317
402
|
return (
|
|
@@ -334,57 +419,36 @@ export function DocumentCaptureStep(): React.ReactElement {
|
|
|
334
419
|
);
|
|
335
420
|
}
|
|
336
421
|
const isBack = phase === 'back';
|
|
422
|
+
|
|
337
423
|
return (
|
|
338
|
-
|
|
424
|
+
<>
|
|
339
425
|
{cropper}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
sideBadge={isTwoSided ? (isBack ? 'Back Side' : 'Front Side') : undefined}
|
|
344
|
-
stepLabel={isTwoSided ? (isBack ? 'Step 2 of 2' : 'Step 1 of 2') : undefined}
|
|
345
|
-
/>
|
|
346
|
-
{isBack ? (
|
|
347
|
-
<View style={{ flexDirection: 'row', alignItems: 'center', marginTop: spacing.sm, marginBottom: spacing.xs }}>
|
|
348
|
-
<Icon name="credit-card" size={14} color={colors.primary} />
|
|
349
|
-
<View style={{ width: 4 }} />
|
|
350
|
-
<MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '500' }}>
|
|
351
|
-
Flip the card over and scan the other side
|
|
352
|
-
</MyazaText>
|
|
353
|
-
</View>
|
|
354
|
-
) : null}
|
|
355
|
-
<View style={{ height: spacing.md }} />
|
|
356
|
-
<CameraViewfinder
|
|
426
|
+
<CameraPhase
|
|
427
|
+
isBack={isBack}
|
|
428
|
+
cameraLive={cameraLive}
|
|
357
429
|
active={perm === 'granted'}
|
|
358
|
-
side={isBack ? 'back' : 'front'}
|
|
359
430
|
documentLabel={documentLabel}
|
|
360
431
|
guideAspect={guideAspect}
|
|
361
|
-
|
|
432
|
+
isTwoSided={isTwoSided}
|
|
362
433
|
busy={busy}
|
|
434
|
+
allowUpload={allowUpload}
|
|
435
|
+
country={country}
|
|
436
|
+
idType={selectedIdType ?? undefined}
|
|
437
|
+
mrzAlreadyCaptured={!!mrzScan}
|
|
438
|
+
onMrz={setMrzScan}
|
|
439
|
+
onCapture={(uri, videoPath, viewAr) => void captureFromCamera(uri, videoPath, viewAr)}
|
|
440
|
+
onUpload={pickFromGallery}
|
|
441
|
+
onBack={() => store.getState().previousStep()}
|
|
442
|
+
// The camera is a TRUE full-screen modal now, so it runs under the
|
|
443
|
+
// home indicator and has to clear the real inset — the smaller value
|
|
444
|
+
// that suited the inset sheet leaves the shutter under the gesture bar.
|
|
445
|
+
bottomInset={cameraBottomInset()}
|
|
363
446
|
/>
|
|
364
|
-
|
|
365
|
-
<>
|
|
366
|
-
<View style={{ height: spacing.md }} />
|
|
367
|
-
<MyazaText variant="bodySmall" style={{ textAlign: 'center' }}>
|
|
368
|
-
Tap the button to capture manually
|
|
369
|
-
</MyazaText>
|
|
370
|
-
{allowUpload ? (
|
|
371
|
-
<Pressable onPress={pickFromGallery} style={{ marginTop: spacing.sm }}>
|
|
372
|
-
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
|
|
373
|
-
<MyazaText variant="bodySmall">Having trouble? </MyazaText>
|
|
374
|
-
<Icon name="upload" size={14} color={colors.primary} />
|
|
375
|
-
<View style={{ width: 4 }} />
|
|
376
|
-
<MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
|
|
377
|
-
Upload a photo instead
|
|
378
|
-
</MyazaText>
|
|
379
|
-
</View>
|
|
380
|
-
</Pressable>
|
|
381
|
-
) : null}
|
|
382
|
-
</>
|
|
383
|
-
) : null}
|
|
384
|
-
</View>
|
|
447
|
+
</>
|
|
385
448
|
);
|
|
386
449
|
}
|
|
387
450
|
|
|
451
|
+
|
|
388
452
|
// ── Front preview (two-sided) ──────────────────────────────────────────────
|
|
389
453
|
if (phase === 'front-preview') {
|
|
390
454
|
return (
|
|
@@ -404,84 +468,44 @@ export function DocumentCaptureStep(): React.ReactElement {
|
|
|
404
468
|
|
|
405
469
|
// ── Review ─────────────────────────────────────────────────────────────────
|
|
406
470
|
return (
|
|
407
|
-
<
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
{
|
|
471
|
+
<DocumentReview
|
|
472
|
+
frontUri={frontUri!}
|
|
473
|
+
backUri={isTwoSided ? backUri : null}
|
|
474
|
+
aspect={guideAspect}
|
|
475
|
+
isBusy={uploading}
|
|
476
|
+
busyOverlay={
|
|
477
|
+
// Standard upload loader — a dark scrim + the pulse-ring loader, drawn
|
|
478
|
+
// INSIDE the preview frame, mirroring the web/Flutter SDKs (and the
|
|
479
|
+
// liveness selfie review).
|
|
480
|
+
<View
|
|
481
|
+
style={{
|
|
482
|
+
flex: 1,
|
|
483
|
+
alignItems: 'center',
|
|
484
|
+
justifyContent: 'center',
|
|
485
|
+
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
486
|
+
}}
|
|
487
|
+
>
|
|
488
|
+
<MyazaPulseLoader size={48} />
|
|
489
|
+
</View>
|
|
490
|
+
}
|
|
491
|
+
onRetakeFront={() => retake('front')}
|
|
492
|
+
onRetakeBack={() => retake('back')}
|
|
493
|
+
footer={
|
|
411
494
|
<>
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
</>
|
|
416
|
-
) : null}
|
|
417
|
-
|
|
418
|
-
{retryInfo && uploading ? (
|
|
419
|
-
<MyazaText variant="bodySmall" color={colors.warning} style={{ textAlign: 'center', marginTop: spacing.sm }}>
|
|
420
|
-
{`Upload failed — retrying (${retryInfo.attempt}/${retryInfo.total})…`}
|
|
421
|
-
</MyazaText>
|
|
422
|
-
) : null}
|
|
423
|
-
|
|
424
|
-
<View style={{ height: spacing.md }} />
|
|
425
|
-
{uploadError ? (
|
|
426
|
-
// The error message is shown as a top toast; keep a retry action here.
|
|
427
|
-
<MyazaButton label="Try Again" onPress={handleContinue} loading={uploading} />
|
|
428
|
-
) : (
|
|
429
|
-
<MyazaButton label="Continue" onPress={handleContinue} loading={uploading} />
|
|
430
|
-
)}
|
|
431
|
-
</View>
|
|
432
|
-
);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// "Required: {label}" pill with optional side badge + step label — mirrors the
|
|
436
|
-
// Flutter SDK's _RequiredPill on the capture screen.
|
|
437
|
-
function RequiredPill({
|
|
438
|
-
documentLabel,
|
|
439
|
-
sideBadge,
|
|
440
|
-
stepLabel,
|
|
441
|
-
}: {
|
|
442
|
-
documentLabel: string;
|
|
443
|
-
sideBadge?: string;
|
|
444
|
-
stepLabel?: string;
|
|
445
|
-
}): React.ReactElement {
|
|
446
|
-
const { colors } = useTheme();
|
|
447
|
-
return (
|
|
448
|
-
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
449
|
-
<View
|
|
450
|
-
style={{
|
|
451
|
-
flex: 1,
|
|
452
|
-
flexDirection: 'row',
|
|
453
|
-
alignItems: 'center',
|
|
454
|
-
flexWrap: 'wrap',
|
|
455
|
-
borderWidth: 1,
|
|
456
|
-
borderColor: `${colors.primary}33`,
|
|
457
|
-
backgroundColor: `${colors.primary}0D`,
|
|
458
|
-
borderRadius: radius.md,
|
|
459
|
-
paddingHorizontal: spacing.sm + 4,
|
|
460
|
-
paddingVertical: spacing.sm,
|
|
461
|
-
}}
|
|
462
|
-
>
|
|
463
|
-
<Icon name="credit-card" size={16} color={colors.primary} />
|
|
464
|
-
<View style={{ width: 8 }} />
|
|
465
|
-
<MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>
|
|
466
|
-
Required:{' '}
|
|
467
|
-
</MyazaText>
|
|
468
|
-
<MyazaText variant="bodySmall" color={colors.primary} style={{ flexShrink: 1 }}>
|
|
469
|
-
{documentLabel}
|
|
470
|
-
</MyazaText>
|
|
471
|
-
{sideBadge ? (
|
|
472
|
-
<View style={{ backgroundColor: `${colors.primary}26`, borderRadius: radius.full, paddingHorizontal: 8, paddingVertical: 2, marginLeft: 8 }}>
|
|
473
|
-
<MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', fontSize: 11 }}>
|
|
474
|
-
{sideBadge}
|
|
495
|
+
{retryInfo && uploading ? (
|
|
496
|
+
<MyazaText variant="bodySmall" color={colors.warning} style={{ textAlign: 'center', marginBottom: spacing.sm }}>
|
|
497
|
+
{`Upload failed — retrying (${retryInfo.attempt}/${retryInfo.total})…`}
|
|
475
498
|
</MyazaText>
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
499
|
+
) : null}
|
|
500
|
+
{uploadError ? (
|
|
501
|
+
// The error message is shown as a top toast; keep a retry action here.
|
|
502
|
+
<MyazaButton label="Try Again" onPress={handleContinue} loading={uploading} />
|
|
503
|
+
) : (
|
|
504
|
+
<MyazaButton label="Continue" onPress={handleContinue} loading={uploading} />
|
|
505
|
+
)}
|
|
506
|
+
</>
|
|
507
|
+
}
|
|
508
|
+
/>
|
|
485
509
|
);
|
|
486
510
|
}
|
|
487
511
|
|
|
@@ -5,7 +5,7 @@ import { spacing } from '../config/theme';
|
|
|
5
5
|
import { ID_TYPES } from '../config/idTypes';
|
|
6
6
|
import { validateIdNumber } from '../services/validators';
|
|
7
7
|
import type { IdType } from '../types/config';
|
|
8
|
-
import { useKyc, useKycConfig } from '../components/runtime';
|
|
8
|
+
import { useEffectiveCountry, useKyc, useKycConfig } from '../components/runtime';
|
|
9
9
|
import { MyazaText } from '../components/Typography';
|
|
10
10
|
import { MyazaInput } from '../components/MyazaInput';
|
|
11
11
|
import { MyazaButton } from '../components/MyazaButton';
|
|
@@ -24,11 +24,16 @@ function hintFor(def: { label: string; inputLabel?: string; digits?: number } |
|
|
|
24
24
|
|
|
25
25
|
export function IdInputStep(): React.ReactElement {
|
|
26
26
|
const config = useKycConfig();
|
|
27
|
+
const country = useEffectiveCountry();
|
|
27
28
|
const selectedIdType = useKyc((s) => s.selectedIdType);
|
|
29
|
+
const storedIdNumber = useKyc((s) => s.idNumber);
|
|
28
30
|
const setIdNumber = useKyc((s) => s.setIdNumber);
|
|
29
31
|
const nextStep = useKyc((s) => s.nextStep);
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
// Seeded from the store so a back-and-forward through the flow never loses
|
|
34
|
+
// what was already typed — everything persists until submission, or until
|
|
35
|
+
// the user themselves changes it (mirrors web + Flutter).
|
|
36
|
+
const [value, setValue] = useState(storedIdNumber ?? '');
|
|
32
37
|
const [error, setError] = useState<string | null>(null);
|
|
33
38
|
|
|
34
39
|
const def = useMemo(() => Object.values(ID_TYPES).flat().find((t) => t.key === selectedIdType) ?? null, [selectedIdType]);
|
|
@@ -36,15 +41,18 @@ export function IdInputStep(): React.ReactElement {
|
|
|
36
41
|
|
|
37
42
|
const valid = useMemo(() => {
|
|
38
43
|
if (!selectedIdType || !value.trim()) return false;
|
|
39
|
-
return validateIdNumber(
|
|
40
|
-
}, [
|
|
44
|
+
return validateIdNumber(country, selectedIdType as IdType, value).valid;
|
|
45
|
+
}, [country, selectedIdType, value]);
|
|
41
46
|
|
|
42
47
|
const handleChange = (text: string) => {
|
|
43
48
|
// Match Flutter: BVN/NIN are digits-only.
|
|
44
49
|
const next = isDigits ? text.replace(/[^0-9]/g, '') : text;
|
|
45
50
|
setValue(next);
|
|
51
|
+
// Write-through to the store as they type — a back-swipe mid-entry must
|
|
52
|
+
// not cost them the digits they already entered.
|
|
53
|
+
setIdNumber(next);
|
|
46
54
|
if (selectedIdType && next.trim()) {
|
|
47
|
-
const result = validateIdNumber(
|
|
55
|
+
const result = validateIdNumber(country, selectedIdType as IdType, next);
|
|
48
56
|
setError(result.valid ? null : result.message ?? null);
|
|
49
57
|
} else {
|
|
50
58
|
setError(null);
|
|
@@ -2,16 +2,35 @@ import React, { useMemo } from 'react';
|
|
|
2
2
|
import { Pressable, View } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { radius, spacing } from '../config/theme';
|
|
5
|
-
import { ID_TYPES } from '../config/idTypes';
|
|
5
|
+
import { ID_TYPES, resolveIdTypeDefinition } from '../config/idTypes';
|
|
6
6
|
import type { IdType, IdTypeDefinition } from '../types/config';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
useEffectiveCountry,
|
|
9
|
+
useKyc,
|
|
10
|
+
useKycConfig,
|
|
11
|
+
useKycStore,
|
|
12
|
+
useTheme,
|
|
13
|
+
} from '../components/runtime';
|
|
8
14
|
import { MyazaText } from '../components/Typography';
|
|
9
|
-
import { MyazaButton } from '../components/MyazaButton';
|
|
10
15
|
import { MyazaPulseLoader } from '../components/MyazaPulseLoader';
|
|
11
16
|
import { Icon, type IconName } from '../components/Icon';
|
|
12
17
|
|
|
13
18
|
// ID-type picker — 1:1 with the Flutter SDK's IdTypeScreen: a list of cards
|
|
14
|
-
// (icon tile + label +
|
|
19
|
+
// (icon tile + label + forward chevron) that ADVANCE ON TAP.
|
|
20
|
+
//
|
|
21
|
+
// No Continue button and no radio, matching Flutter and the web SDK. It is a
|
|
22
|
+
// single-select list with nothing else on the step to confirm, so Continue only
|
|
23
|
+
// restated a decision already made; and country-select already advances on tap,
|
|
24
|
+
// so a second list that behaved differently taught users one rule then broke
|
|
25
|
+
// it. A mis-tap costs one Back. The affordance is therefore a "go to the next
|
|
26
|
+
// step" chevron — a radio would imply a select-then-confirm this flow does not
|
|
27
|
+
// have.
|
|
28
|
+
//
|
|
29
|
+
// The advance reads the TAPPED value through the store, never the
|
|
30
|
+
// `selectedIdType` bound at render: that closure is a step behind inside the
|
|
31
|
+
// handler (and empty on the first selection), which would route a document ID
|
|
32
|
+
// to the number-only step. Going through `store.getState()` is what makes the
|
|
33
|
+
// write visible to `nextStep()`, since zustand commits synchronously.
|
|
15
34
|
|
|
16
35
|
const ICON_FOR: Record<string, IconName> = {
|
|
17
36
|
bvn: 'landmark', // Bank Verification Number → bank/landmark
|
|
@@ -31,25 +50,54 @@ const ICON_FOR: Record<string, IconName> = {
|
|
|
31
50
|
export function IdTypeStep(): React.ReactElement {
|
|
32
51
|
const { colors } = useTheme();
|
|
33
52
|
const config = useKycConfig();
|
|
53
|
+
const country = useEffectiveCountry();
|
|
54
|
+
const store = useKycStore();
|
|
34
55
|
const serverConfig = useKyc((s) => s.serverConfig);
|
|
35
56
|
const selectedIdType = useKyc((s) => s.selectedIdType);
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
|
|
58
|
+
// Select AND advance in one tap. Both go through the store so `nextStep`
|
|
59
|
+
// routes off the value just written — a document ID to document-capture, a
|
|
60
|
+
// number-only ID to id-input.
|
|
61
|
+
const pick = (key: IdType): void => {
|
|
62
|
+
store.getState().setIdType(key);
|
|
63
|
+
store.getState().nextStep();
|
|
64
|
+
};
|
|
38
65
|
|
|
39
66
|
const available = useMemo<IdTypeDefinition[]>(() => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
67
|
+
// In a multi-region flow the PICKED country's own `idTypes` narrow the
|
|
68
|
+
// list; otherwise the top-level prop applies. Absent or empty means every
|
|
69
|
+
// granted type, which is the server's own "unset = all" semantic.
|
|
70
|
+
const perCountry = (config.countries ?? []).find(
|
|
71
|
+
(e) => e.country.toUpperCase() === country.toUpperCase(),
|
|
72
|
+
)?.idTypes;
|
|
73
|
+
const keys = (perCountry?.length ? perCountry : config.idTypes) as
|
|
74
|
+
| readonly string[]
|
|
75
|
+
| undefined;
|
|
76
|
+
const allowed = (key: string): boolean => !keys?.length || keys.includes(key);
|
|
77
|
+
|
|
78
|
+
// Built from the SERVER rows, not the curated table. The curated table only
|
|
79
|
+
// knows the five gov-DB countries; a Global-Documents flow offers ~240, and
|
|
80
|
+
// filtering against the table rendered an empty list — "no ID types are
|
|
81
|
+
// enabled" — for every country outside it. The row carries the label and
|
|
82
|
+
// capture shape, so an uncurated country renders correctly from it.
|
|
44
83
|
if (serverConfig.status === 'ready') {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
84
|
+
return serverConfig.idTypes
|
|
85
|
+
.filter((row) => row.country === country && allowed(row.idType))
|
|
86
|
+
.map((row) =>
|
|
87
|
+
resolveIdTypeDefinition(country, row.idType, {
|
|
88
|
+
label: row.label,
|
|
89
|
+
requiresDocumentCapture: row.requiresDocumentCapture,
|
|
90
|
+
scanSides: row.scanSides,
|
|
91
|
+
supportsNfc: row.supportsNfc,
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
49
94
|
}
|
|
50
95
|
if (serverConfig.status === 'loading') return [];
|
|
51
|
-
|
|
52
|
-
|
|
96
|
+
// Config failed to load: fall back to the curated list. The server still
|
|
97
|
+
// rejects anything actually disabled, so this is permissive, not unsafe.
|
|
98
|
+
const table = ID_TYPES as Record<string, readonly IdTypeDefinition[] | undefined>;
|
|
99
|
+
return (table[country.toUpperCase()] ?? []).filter((t) => allowed(t.key));
|
|
100
|
+
}, [country, config.idTypes, config.countries, serverConfig]);
|
|
53
101
|
|
|
54
102
|
if (serverConfig.status === 'loading') {
|
|
55
103
|
return (
|
|
@@ -84,7 +132,9 @@ export function IdTypeStep(): React.ReactElement {
|
|
|
84
132
|
return (
|
|
85
133
|
<Pressable
|
|
86
134
|
key={t.key}
|
|
87
|
-
onPress={() =>
|
|
135
|
+
onPress={() => pick(t.key)}
|
|
136
|
+
accessibilityRole="button"
|
|
137
|
+
accessibilityLabel={t.label}
|
|
88
138
|
style={{
|
|
89
139
|
marginBottom: spacing.sm,
|
|
90
140
|
borderRadius: radius.md,
|
|
@@ -118,25 +168,16 @@ export function IdTypeStep(): React.ReactElement {
|
|
|
118
168
|
{t.label}
|
|
119
169
|
</MyazaText>
|
|
120
170
|
<View style={{ width: spacing.md }} />
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
alignItems: 'center',
|
|
129
|
-
justifyContent: 'center',
|
|
130
|
-
}}
|
|
131
|
-
>
|
|
132
|
-
{selected ? <View style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: colors.primary }} /> : null}
|
|
133
|
-
</View>
|
|
171
|
+
{/* Forward chevron, not a radio: the tap advances, so the icon has
|
|
172
|
+
to promise the next step rather than a pending confirmation. */}
|
|
173
|
+
<Icon
|
|
174
|
+
name="chevron-right"
|
|
175
|
+
size={20}
|
|
176
|
+
color={selected ? colors.primary : colors.gray400}
|
|
177
|
+
/>
|
|
134
178
|
</Pressable>
|
|
135
179
|
);
|
|
136
180
|
})}
|
|
137
|
-
|
|
138
|
-
<View style={{ height: spacing.lg }} />
|
|
139
|
-
<MyazaButton label="Continue" onPress={selectedIdType ? nextStep : undefined} disabled={!selectedIdType} />
|
|
140
181
|
</View>
|
|
141
182
|
);
|
|
142
183
|
}
|