@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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import { useStore } from 'zustand';
|
|
4
|
+
import Svg, { Path, Rect } from 'react-native-svg';
|
|
5
|
+
|
|
6
|
+
import { holePath, type FlashHole } from './flashHoleGeometry';
|
|
7
|
+
import { useKycStore } from './runtime';
|
|
8
|
+
import { MyazaText } from './Typography';
|
|
9
|
+
import { spacing } from '../config/theme';
|
|
10
|
+
|
|
11
|
+
export type { FlashHole } from './flashHoleGeometry';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// The flash overlay: a full-screen colour with a hole over the camera preview.
|
|
15
|
+
//
|
|
16
|
+
// The screen IS the light source. How much of it is lit is not cosmetic — it
|
|
17
|
+
// decides how much light reaches the face, and therefore whether the reflection
|
|
18
|
+
// is measurable at all. An overlay confined to the step's own body only lit the
|
|
19
|
+
// inset sheet, leaving the rest of the display dark; that is a fraction of the
|
|
20
|
+
// available light and it made flashes read as inconclusive.
|
|
21
|
+
//
|
|
22
|
+
// So it paints at the SHEET ROOT — over the header, the body and the footer,
|
|
23
|
+
// but inside the same tree as the preview.
|
|
24
|
+
//
|
|
25
|
+
// Not in a modal of its own, which is the obvious way to reach the physical
|
|
26
|
+
// screen edges and does not work: iOS scales a presenting sheet back when a
|
|
27
|
+
// modal appears over it, and RN's `measureInWindow` reports Yoga LAYOUT
|
|
28
|
+
// coordinates, which never reflect that UIKit transform. The cutout came back
|
|
29
|
+
// as exactly the preview's layout size (300pt) while the preview was rendering
|
|
30
|
+
// at ~232pt — off by the transform, and no re-measuring can converge on a value
|
|
31
|
+
// the measurement API cannot observe.
|
|
32
|
+
//
|
|
33
|
+
// Painting in the same tree makes the hole and the preview scale TOGETHER, so
|
|
34
|
+
// they agree by construction rather than by measurement. The cost is the strip
|
|
35
|
+
// above an iOS page sheet, which stays unlit — a bounded, predictable loss,
|
|
36
|
+
// where a misaligned hole is an unbounded one.
|
|
37
|
+
//
|
|
38
|
+
// A hole is not required for correctness: light lost through it is constant
|
|
39
|
+
// across the neutral and lit windows, so it cancels in the comparison either
|
|
40
|
+
// way. If measurement fails the overlay paints solid — the user loses sight of
|
|
41
|
+
// their own framing for about a second, which is worth far less than failing
|
|
42
|
+
// the check outright. Flutter takes the same fallback.
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Rendered once at the sheet root.
|
|
47
|
+
*
|
|
48
|
+
* It subscribes to the paint slot ITSELF rather than taking props, so a colour
|
|
49
|
+
* change re-renders this leaf alone. Threading the colour down through the sheet
|
|
50
|
+
* would re-render the step on every flash — and the camera with it, mid-capture.
|
|
51
|
+
*/
|
|
52
|
+
export function FlashOverlay(): React.ReactElement | null {
|
|
53
|
+
const store = useKycStore();
|
|
54
|
+
const paint = useStore(store, (s) => s.flashPaint);
|
|
55
|
+
const [size, setSize] = useState<{ width: number; height: number } | null>(null);
|
|
56
|
+
|
|
57
|
+
if (!paint) return null;
|
|
58
|
+
const { color, hole } = paint;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<View
|
|
62
|
+
testID="kyc.liveness.flash"
|
|
63
|
+
pointerEvents="none"
|
|
64
|
+
onLayout={(e) => {
|
|
65
|
+
const { width, height } = e.nativeEvent.layout;
|
|
66
|
+
if (size?.width !== width || size?.height !== height) setSize({ width, height });
|
|
67
|
+
}}
|
|
68
|
+
// zIndex covers iOS sibling stacking; elevation covers Android, where a
|
|
69
|
+
// sibling WITH elevation (the glass header) otherwise paints above any
|
|
70
|
+
// absolute overlay that has none.
|
|
71
|
+
style={[StyleSheet.absoluteFill, { zIndex: 100, elevation: 100 }]}
|
|
72
|
+
>
|
|
73
|
+
{color && size ? (
|
|
74
|
+
<Panels color={color} hole={hole} width={size.width} height={size.height} />
|
|
75
|
+
) : null}
|
|
76
|
+
</View>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The colour, with a CIRCULAR bite taken out of it.
|
|
82
|
+
*
|
|
83
|
+
* Four rectangles around the preview were cheaper, but they can only ever
|
|
84
|
+
* describe a rectangle — against a circular preview that leaves the corners
|
|
85
|
+
* painted over the user's face, which is precisely the part they need to see to
|
|
86
|
+
* stay framed.
|
|
87
|
+
*
|
|
88
|
+
* A single even-odd path is the actual shape: the screen rectangle and the
|
|
89
|
+
* preview circle as two subpaths, so the circle is subtracted rather than drawn.
|
|
90
|
+
* One node, no mask, no per-frame compositing.
|
|
91
|
+
*/
|
|
92
|
+
function Panels({
|
|
93
|
+
color,
|
|
94
|
+
hole,
|
|
95
|
+
width,
|
|
96
|
+
height,
|
|
97
|
+
}: {
|
|
98
|
+
color: string;
|
|
99
|
+
hole: FlashHole | null;
|
|
100
|
+
width: number;
|
|
101
|
+
height: number;
|
|
102
|
+
}): React.ReactElement {
|
|
103
|
+
return (
|
|
104
|
+
<>
|
|
105
|
+
<Svg width={width} height={height} style={{ position: 'absolute', top: 0, left: 0 }}>
|
|
106
|
+
{hole ? (
|
|
107
|
+
<Path
|
|
108
|
+
// Screen rect, then the circle as two arcs. `evenodd` is what makes
|
|
109
|
+
// the second subpath a hole instead of a second filled shape.
|
|
110
|
+
d={holePath(width, height, hole)}
|
|
111
|
+
fill={color}
|
|
112
|
+
fillRule="evenodd"
|
|
113
|
+
/>
|
|
114
|
+
) : (
|
|
115
|
+
// Measurement failed — paint solid. The face is still lit, which is
|
|
116
|
+
// what the check needs; only the framing preview is lost.
|
|
117
|
+
<Rect x={0} y={0} width={width} height={height} fill={color} />
|
|
118
|
+
)}
|
|
119
|
+
</Svg>
|
|
120
|
+
|
|
121
|
+
<HoldStill hole={hole} width={width} height={height} />
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The one instruction shown during a flash.
|
|
128
|
+
*
|
|
129
|
+
* Anchored near the BOTTOM of the sheet, deliberately not hugging the cutout:
|
|
130
|
+
* text directly above and below the preview crowded the only part of the screen
|
|
131
|
+
* the user is meant to be looking at, and read as two competing messages.
|
|
132
|
+
*
|
|
133
|
+
* Clamped to sit below the cutout as well, so on a short screen — where the
|
|
134
|
+
* preview sits low — it moves down rather than landing on the face.
|
|
135
|
+
*/
|
|
136
|
+
function HoldStill({
|
|
137
|
+
hole,
|
|
138
|
+
width,
|
|
139
|
+
height,
|
|
140
|
+
}: {
|
|
141
|
+
hole: FlashHole | null;
|
|
142
|
+
width: number;
|
|
143
|
+
height: number;
|
|
144
|
+
}): React.ReactElement {
|
|
145
|
+
const belowHole = hole ? hole.y + hole.size + spacing.xl : 0;
|
|
146
|
+
const top = Math.min(Math.max(belowHole, height * 0.78), height - spacing.xl * 2);
|
|
147
|
+
return (
|
|
148
|
+
<View style={{ position: 'absolute', top, left: 0, width, alignItems: 'center' }}>
|
|
149
|
+
{/* Dark text: every palette colour is high-luminance, so white would be
|
|
150
|
+
unreadable on cyan and green. */}
|
|
151
|
+
<MyazaText variant="heading3" color="rgba(0,0,0,0.8)" style={{ textAlign: 'center' }}>
|
|
152
|
+
Hold still
|
|
153
|
+
</MyazaText>
|
|
154
|
+
</View>
|
|
155
|
+
);
|
|
156
|
+
}
|
package/src/components/Icon.tsx
CHANGED
|
@@ -3,8 +3,13 @@ import Ionicons from '@expo/vector-icons/Ionicons';
|
|
|
3
3
|
import {
|
|
4
4
|
BadgeCheck,
|
|
5
5
|
BookUser,
|
|
6
|
+
Building2,
|
|
7
|
+
Calendar,
|
|
6
8
|
Camera,
|
|
7
9
|
CameraOff,
|
|
10
|
+
ChevronDown,
|
|
11
|
+
ChevronLeft,
|
|
12
|
+
ChevronRight,
|
|
8
13
|
Car,
|
|
9
14
|
Check,
|
|
10
15
|
CircleAlert,
|
|
@@ -13,19 +18,36 @@ import {
|
|
|
13
18
|
Fingerprint,
|
|
14
19
|
FileText,
|
|
15
20
|
IdCard,
|
|
21
|
+
Image as ImageIcon,
|
|
16
22
|
Landmark,
|
|
17
23
|
Lightbulb,
|
|
18
24
|
Globe,
|
|
25
|
+
Mail,
|
|
26
|
+
Maximize2,
|
|
19
27
|
Lock,
|
|
28
|
+
MessageCircle,
|
|
29
|
+
MessageSquare,
|
|
20
30
|
Moon,
|
|
21
31
|
MoveLeft,
|
|
32
|
+
Nfc,
|
|
33
|
+
Pencil,
|
|
34
|
+
Smartphone,
|
|
22
35
|
RefreshCw,
|
|
23
36
|
ScanFace,
|
|
24
37
|
ScanLine,
|
|
38
|
+
Search,
|
|
39
|
+
Zap,
|
|
40
|
+
ZapOff,
|
|
25
41
|
ShieldCheck,
|
|
26
42
|
Sun,
|
|
43
|
+
Timer,
|
|
27
44
|
Upload,
|
|
28
45
|
UserRound,
|
|
46
|
+
UserRoundPlus,
|
|
47
|
+
Link2,
|
|
48
|
+
Copy,
|
|
49
|
+
Share2,
|
|
50
|
+
UsersRound,
|
|
29
51
|
VideoOff,
|
|
30
52
|
Vote,
|
|
31
53
|
X,
|
|
@@ -47,12 +69,30 @@ export type IconName =
|
|
|
47
69
|
| 'lock'
|
|
48
70
|
| 'alert'
|
|
49
71
|
| 'refresh'
|
|
72
|
+
| 'maximize'
|
|
73
|
+
| 'calendar'
|
|
74
|
+
| 'image'
|
|
75
|
+
| 'chevron-left'
|
|
76
|
+
| 'chevron-right'
|
|
77
|
+
| 'chevron-down'
|
|
50
78
|
// consent hero + process steps
|
|
51
79
|
| 'shield'
|
|
52
80
|
| 'badge-check'
|
|
81
|
+
| 'building-2'
|
|
53
82
|
| 'user'
|
|
83
|
+
| 'user-plus'
|
|
84
|
+
| 'users'
|
|
54
85
|
| 'scan-line'
|
|
55
86
|
| 'scan-face'
|
|
87
|
+
| 'nfc'
|
|
88
|
+
| 'pencil'
|
|
89
|
+
| 'search'
|
|
90
|
+
| 'zap'
|
|
91
|
+
| 'link'
|
|
92
|
+
| 'copy'
|
|
93
|
+
| 'share'
|
|
94
|
+
| 'zap-off'
|
|
95
|
+
| 'timer'
|
|
56
96
|
// id-type glyphs
|
|
57
97
|
| 'fingerprint'
|
|
58
98
|
| 'credit-card'
|
|
@@ -68,7 +108,13 @@ export type IconName =
|
|
|
68
108
|
| 'camera-off'
|
|
69
109
|
| 'video-off'
|
|
70
110
|
| 'upload'
|
|
71
|
-
| '
|
|
111
|
+
| 'x'
|
|
112
|
+
| 'lightbulb'
|
|
113
|
+
// contact verification (OTP channels + step footer)
|
|
114
|
+
| 'mail'
|
|
115
|
+
| 'smartphone'
|
|
116
|
+
| 'message-square'
|
|
117
|
+
| 'message-circle';
|
|
72
118
|
|
|
73
119
|
// Maps each name to its Lucide component. Names mirror the Flutter SDK exactly:
|
|
74
120
|
// shieldCheck · badgeCheck · userRound · scanLine · scanFace · fingerprint ·
|
|
@@ -83,11 +129,29 @@ const ICONS: Record<IconName, LucideIcon> = {
|
|
|
83
129
|
lock: Lock,
|
|
84
130
|
alert: CircleAlert,
|
|
85
131
|
refresh: RefreshCw,
|
|
132
|
+
maximize: Maximize2,
|
|
133
|
+
calendar: Calendar,
|
|
134
|
+
image: ImageIcon,
|
|
135
|
+
'chevron-left': ChevronLeft,
|
|
136
|
+
'chevron-right': ChevronRight,
|
|
137
|
+
'chevron-down': ChevronDown,
|
|
86
138
|
shield: ShieldCheck,
|
|
87
139
|
'badge-check': BadgeCheck,
|
|
140
|
+
'building-2': Building2,
|
|
88
141
|
user: UserRound,
|
|
142
|
+
'user-plus': UserRoundPlus,
|
|
143
|
+
link: Link2,
|
|
144
|
+
copy: Copy,
|
|
145
|
+
share: Share2,
|
|
146
|
+
users: UsersRound,
|
|
89
147
|
'scan-line': ScanLine,
|
|
90
148
|
'scan-face': ScanFace,
|
|
149
|
+
nfc: Nfc,
|
|
150
|
+
pencil: Pencil,
|
|
151
|
+
search: Search,
|
|
152
|
+
zap: Zap,
|
|
153
|
+
'zap-off': ZapOff,
|
|
154
|
+
timer: Timer,
|
|
91
155
|
fingerprint: Fingerprint,
|
|
92
156
|
'credit-card': CreditCard,
|
|
93
157
|
globe: Globe,
|
|
@@ -102,7 +166,12 @@ const ICONS: Record<IconName, LucideIcon> = {
|
|
|
102
166
|
'camera-off': CameraOff,
|
|
103
167
|
'video-off': VideoOff,
|
|
104
168
|
upload: Upload,
|
|
169
|
+
x: X,
|
|
105
170
|
lightbulb: Lightbulb,
|
|
171
|
+
mail: Mail,
|
|
172
|
+
smartphone: Smartphone,
|
|
173
|
+
'message-square': MessageSquare,
|
|
174
|
+
'message-circle': MessageCircle,
|
|
106
175
|
};
|
|
107
176
|
|
|
108
177
|
export interface IconProps {
|
|
@@ -2,32 +2,27 @@ import React, { useEffect, useMemo, useRef } from 'react';
|
|
|
2
2
|
import { View } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { radius, spacing } from '../config/theme';
|
|
5
|
-
import {
|
|
5
|
+
import { buildStepOrder } from '../config/stepOrder';
|
|
6
|
+
import { stepOrderOptions } from '../store/kycStore';
|
|
6
7
|
import { KYCError } from '../types/verification';
|
|
7
8
|
import { safeReportError } from '../services/errors';
|
|
8
9
|
import type { KYCStep, SupportedCountry } from '../types/config';
|
|
9
|
-
import { useKyc, useKycConfig, useKycStore, useTheme } from './runtime';
|
|
10
|
+
import { useEffectiveCountry, useKyc, useKycConfig, useKycStore, useTheme } from './runtime';
|
|
11
|
+
import { COUNTRY_SEARCH_THRESHOLD } from '../screens/CountrySelectStep';
|
|
10
12
|
import { KycSheet } from './KycSheet';
|
|
13
|
+
import { FatalConfigError } from './flow/FatalConfigError';
|
|
11
14
|
import { ToastProvider } from './toast';
|
|
12
15
|
import { MyazaText } from './Typography';
|
|
13
16
|
import { MyazaButton } from './MyazaButton';
|
|
14
17
|
import { Icon } from './Icon';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { DocumentCaptureStep, documentCaptureMeta } from '../screens/DocumentCaptureStep';
|
|
19
|
-
import { LivenessStep } from '../screens/LivenessStep';
|
|
20
|
-
import { SubmittedStep } from '../screens/SubmittedStep';
|
|
18
|
+
import { StepView } from './StepView';
|
|
19
|
+
import { stepHeaderMeta } from './stepHeaderMeta';
|
|
20
|
+
import { countrySelectOptions } from '../store/derive';
|
|
21
21
|
|
|
22
22
|
// The step router — 1:1 with the Flutter SDK's _KycFlowWidget. Computes per-step
|
|
23
23
|
// header title/description, the 4-step indicator info, back/country, fetches the
|
|
24
24
|
// server config on mount, and gates the flow on fatal auth failures.
|
|
25
25
|
|
|
26
|
-
function labelFor(country: SupportedCountry, idType: string | null): string {
|
|
27
|
-
if (!idType) return 'Document';
|
|
28
|
-
return Object.values(ID_TYPES).flat().find((t) => t.key === idType)?.label ?? 'Document';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
26
|
/**
|
|
32
27
|
* Result of a back request handled inside the flow:
|
|
33
28
|
* 'navigated' — went back a step; the caller should NOT close.
|
|
@@ -47,11 +42,14 @@ export function KycFlow({
|
|
|
47
42
|
}): React.ReactElement {
|
|
48
43
|
const { colors } = useTheme();
|
|
49
44
|
const config = useKycConfig();
|
|
45
|
+
const country = useEffectiveCountry();
|
|
50
46
|
const store = useKycStore();
|
|
51
47
|
const currentStep = useKyc((s) => s.currentStep);
|
|
52
48
|
const selectedIdType = useKyc((s) => s.selectedIdType);
|
|
53
49
|
const documentCapturePhase = useKyc((s) => s.documentCapturePhase);
|
|
50
|
+
const contactChallenge = useKyc((s) => s.contactChallenge);
|
|
54
51
|
const serverConfig = useKyc((s) => s.serverConfig);
|
|
52
|
+
const immersiveCapture = useKyc((s) => s.immersiveCapture);
|
|
55
53
|
|
|
56
54
|
const startedRef = useRef(false);
|
|
57
55
|
const reportedRef = useRef(false);
|
|
@@ -60,7 +58,11 @@ export function KycFlow({
|
|
|
60
58
|
if (!startedRef.current) {
|
|
61
59
|
startedRef.current = true;
|
|
62
60
|
config.onStart?.();
|
|
63
|
-
|
|
61
|
+
// A resolved workflow already delivered the allowlist + branding, so
|
|
62
|
+
// there is nothing to fetch.
|
|
63
|
+
if (store.getState().serverConfig.status !== 'ready') {
|
|
64
|
+
void store.getState().loadServerConfig();
|
|
65
|
+
}
|
|
64
66
|
}
|
|
65
67
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
68
|
}, []);
|
|
@@ -75,41 +77,42 @@ export function KycFlow({
|
|
|
75
77
|
}, [isFatal, serverConfig.statusCode, serverConfig.message, config.onError]);
|
|
76
78
|
|
|
77
79
|
// ── Per-step header meta ──────────────────────────────────────────────────
|
|
78
|
-
const meta = useMemo(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// capture screen via `documentCapturePhase`) — mirrors Flutter.
|
|
90
|
-
return documentCaptureMeta(documentCapturePhase, label);
|
|
91
|
-
case 'liveness':
|
|
92
|
-
return { title: 'Face Verification', description: 'Follow the on-screen instructions' };
|
|
93
|
-
case 'submitted':
|
|
94
|
-
default:
|
|
95
|
-
return { title: '', description: null };
|
|
96
|
-
}
|
|
97
|
-
}, [currentStep, config.country, selectedIdType, documentCapturePhase]);
|
|
80
|
+
const meta = useMemo(
|
|
81
|
+
() =>
|
|
82
|
+
stepHeaderMeta(currentStep, {
|
|
83
|
+
config,
|
|
84
|
+
country,
|
|
85
|
+
selectedIdType,
|
|
86
|
+
documentCapturePhase,
|
|
87
|
+
contactChallenge,
|
|
88
|
+
}),
|
|
89
|
+
[currentStep, config, country, selectedIdType, documentCapturePhase, contactChallenge],
|
|
90
|
+
);
|
|
98
91
|
|
|
99
|
-
// ── Step indicator info
|
|
92
|
+
// ── Step indicator info ───────────────────────────────────────────────────
|
|
93
|
+
// Read from the SAME ordered list navigation uses, so the dots can never
|
|
94
|
+
// describe a different flow than the one the user is walking. This used to be
|
|
95
|
+
// its own hand-written copy of the sequence, which meant a step added to the
|
|
96
|
+
// flow silently didn't count towards progress.
|
|
97
|
+
//
|
|
98
|
+
// 'submitted' is excluded on purpose — it is the terminal screen, not a step
|
|
99
|
+
// to make progress towards, and the indicator is hidden there.
|
|
100
|
+
//
|
|
101
|
+
// The derivation is memoised rather than done inside the selector: zustand
|
|
102
|
+
// compares snapshots by identity, so a selector that mints a fresh object on
|
|
103
|
+
// every call re-renders forever.
|
|
104
|
+
const state = useKyc((s) => s);
|
|
100
105
|
const stepInfo = useMemo(() => {
|
|
101
|
-
if (currentStep === 'submitted') return null;
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
const hasLiveness = config.enableLiveness !== false && config.enableSelfie !== false;
|
|
105
|
-
const steps: KYCStep[] = ['consent', 'id-type', hasCapture ? 'document-capture' : 'id-input'];
|
|
106
|
-
if (hasLiveness) steps.push('liveness');
|
|
107
|
-
const idx = steps.indexOf(currentStep);
|
|
106
|
+
if (state.currentStep === 'submitted') return null;
|
|
107
|
+
const steps = buildStepOrder(stepOrderOptions(state)).filter((s) => s !== 'submitted');
|
|
108
|
+
const idx = steps.indexOf(state.currentStep);
|
|
108
109
|
if (idx < 0) return null;
|
|
109
110
|
return { progress: (idx + 1) / steps.length, stepCount: steps.length };
|
|
110
|
-
}, [
|
|
111
|
+
}, [state]);
|
|
111
112
|
|
|
112
|
-
|
|
113
|
+
// The flag beside the title names the country whose IDs are on screen.
|
|
114
|
+
const headerCountry =
|
|
115
|
+
currentStep === 'id-type' || currentStep === 'id-input' ? country : null;
|
|
113
116
|
const onBack = currentStep === 'consent' || currentStep === 'submitted' ? null : () => store.getState().previousStep();
|
|
114
117
|
|
|
115
118
|
// Android hardware back arrives via the <Modal>'s onRequestClose. Expose a
|
|
@@ -136,36 +139,7 @@ export function KycFlow({
|
|
|
136
139
|
}, [backRef, canGoBack, disableClose, store]);
|
|
137
140
|
|
|
138
141
|
if (isFatal) {
|
|
139
|
-
return
|
|
140
|
-
<KycSheet title="" onClose={onClose} hideBrand>
|
|
141
|
-
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
|
142
|
-
<View
|
|
143
|
-
style={{
|
|
144
|
-
width: 80,
|
|
145
|
-
height: 80,
|
|
146
|
-
borderRadius: radius.full,
|
|
147
|
-
backgroundColor: `${colors.error}1A`,
|
|
148
|
-
alignItems: 'center',
|
|
149
|
-
justifyContent: 'center',
|
|
150
|
-
}}
|
|
151
|
-
>
|
|
152
|
-
<Icon name="alert" size={40} color={colors.error} />
|
|
153
|
-
</View>
|
|
154
|
-
<View style={{ height: spacing.lg }} />
|
|
155
|
-
<MyazaText variant="heading2" style={{ textAlign: 'center' }}>
|
|
156
|
-
Unable to start verification
|
|
157
|
-
</MyazaText>
|
|
158
|
-
<View style={{ height: spacing.xs }} />
|
|
159
|
-
<MyazaText variant="bodyMedium" color={colors.textSecondary} style={{ textAlign: 'center' }}>
|
|
160
|
-
{serverConfig.message ?? 'Please check your API key and try again.'}
|
|
161
|
-
</MyazaText>
|
|
162
|
-
<View style={{ height: spacing.lg }} />
|
|
163
|
-
<View style={{ width: '100%' }}>
|
|
164
|
-
<MyazaButton label="Close" onPress={onClose} />
|
|
165
|
-
</View>
|
|
166
|
-
</View>
|
|
167
|
-
</KycSheet>
|
|
168
|
-
);
|
|
142
|
+
return <FatalConfigError message={serverConfig.message} onClose={onClose} />;
|
|
169
143
|
}
|
|
170
144
|
|
|
171
145
|
return (
|
|
@@ -175,31 +149,29 @@ export function KycFlow({
|
|
|
175
149
|
description={meta.description}
|
|
176
150
|
progress={stepInfo?.progress ?? null}
|
|
177
151
|
stepCount={stepInfo?.stepCount ?? null}
|
|
178
|
-
country={
|
|
152
|
+
country={headerCountry}
|
|
179
153
|
onBack={onBack}
|
|
180
154
|
onClose={onClose}
|
|
155
|
+
// The searchable country picker pins its search box above a list that
|
|
156
|
+
// can run to ~240 rows. Only the multi-country variant needs it: a
|
|
157
|
+
// short flat list is happier in the normal scroll body.
|
|
158
|
+
fillsViewport={
|
|
159
|
+
currentStep === 'country-select' &&
|
|
160
|
+
countrySelectOptions({ config, serverConfig }).length > COUNTRY_SEARCH_THRESHOLD
|
|
161
|
+
}
|
|
162
|
+
// A live camera asks for the whole screen: the sheet's header, padding
|
|
163
|
+
// and scroll view are exactly what force a small viewfinder on a short
|
|
164
|
+
// phone, and a camera you have to scroll to is a broken camera.
|
|
165
|
+
//
|
|
166
|
+
// Passed as a PROP rather than rendered as its own tree. The earlier
|
|
167
|
+
// shape early-returned a different element tree, which changed the
|
|
168
|
+
// step's parent MID-STEP — React unmounted and remounted it, wiping the
|
|
169
|
+
// acknowledged primer and bouncing straight back to it. Scoped to the
|
|
170
|
+
// step as well as the flag so the next step cannot inherit it.
|
|
171
|
+
immersive={immersiveCapture && currentStep === 'document-capture'}
|
|
181
172
|
>
|
|
182
173
|
<StepView step={currentStep} onClose={onClose} />
|
|
183
174
|
</KycSheet>
|
|
184
175
|
</ToastProvider>
|
|
185
176
|
);
|
|
186
177
|
}
|
|
187
|
-
|
|
188
|
-
function StepView({ step, onClose }: { step: KYCStep; onClose: () => void }): React.ReactElement {
|
|
189
|
-
switch (step) {
|
|
190
|
-
case 'consent':
|
|
191
|
-
return <ConsentStep />;
|
|
192
|
-
case 'id-type':
|
|
193
|
-
return <IdTypeStep />;
|
|
194
|
-
case 'id-input':
|
|
195
|
-
return <IdInputStep />;
|
|
196
|
-
case 'document-capture':
|
|
197
|
-
return <DocumentCaptureStep />;
|
|
198
|
-
case 'liveness':
|
|
199
|
-
return <LivenessStep />;
|
|
200
|
-
case 'submitted':
|
|
201
|
-
return <SubmittedStep onClose={onClose} />;
|
|
202
|
-
default:
|
|
203
|
-
return <ConsentStep />;
|
|
204
|
-
}
|
|
205
|
-
}
|