@myazahq/kyc-sdk-react-native 2.0.2 → 2.1.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 +111 -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 +67 -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 +152 -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 +331 -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 +151 -0
- package/src/screens/BusinessKeyPersonRow.tsx +170 -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/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,14 +1,18 @@
|
|
|
1
|
-
import React, { useEffect } from 'react';
|
|
2
|
-
import { ActivityIndicator,
|
|
3
|
-
import
|
|
4
|
-
import { Camera, useCameraDevice, usePhotoOutput, useVideoOutput } from 'react-native-vision-camera';
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { ActivityIndicator, Platform, View } from 'react-native';
|
|
3
|
+
import { Camera, useCameraDevice, useCameraDevices, usePhotoOutput, useVideoOutput, type CameraRef } from 'react-native-vision-camera';
|
|
5
4
|
|
|
6
5
|
import { radius, spacing } from '../config/theme';
|
|
6
|
+
import { FRAMED_VIEW_AR } from '../services/cardCrop';
|
|
7
7
|
import { DOCUMENT_VIDEO_BITRATE, VIDEO_CAPTURE_RESOLUTION } from '../config/captureSettings';
|
|
8
8
|
import { useTheme } from './runtime';
|
|
9
|
+
import { documentHintIsAction, documentHintText, useAutoCapture } from '../capture';
|
|
10
|
+
import { shortDocumentLabel } from '../config/idTypes';
|
|
11
|
+
import type { MrzScan } from '../mrz/parse';
|
|
9
12
|
import { useVideoRecorder } from './useVideoRecorder';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
13
|
+
import { CardGuide } from './viewfinder/CardGuide';
|
|
14
|
+
import { HintBanner, OverlayButton, Shutter } from './viewfinder/ViewfinderControls';
|
|
15
|
+
import { ImmersiveOverlay } from './viewfinder/ImmersiveOverlay';
|
|
12
16
|
|
|
13
17
|
// Back-camera viewfinder for document capture — the RN mirror of the Flutter
|
|
14
18
|
// SDK's _DocumentViewfinder. Shows a VisionCamera preview with an ID-card guide
|
|
@@ -17,24 +21,44 @@ import { Icon } from './Icon';
|
|
|
17
21
|
// available" error (no device / init failure) is owned by the parent step, which
|
|
18
22
|
// shows a prominent error + upload fallback. Capture is manual (no edge detection).
|
|
19
23
|
|
|
20
|
-
// Card window within the 3:4 viewBox (centred). Height follows the per-ID guide
|
|
21
|
-
// aspect so the overlay matches the auto-crop (cropCardRegion).
|
|
22
|
-
const VB_W = 100;
|
|
23
|
-
const VB_H = 133;
|
|
24
|
-
const GX = 6;
|
|
25
|
-
const GW = 88;
|
|
26
|
-
const C = 7; // corner accent length
|
|
27
24
|
|
|
28
25
|
export interface CameraViewfinderProps {
|
|
29
26
|
active: boolean;
|
|
30
27
|
side: 'front' | 'back';
|
|
31
28
|
documentLabel: string;
|
|
32
29
|
/** Receives the captured still + a best-effort short document video (or null). */
|
|
33
|
-
onCapture: (
|
|
30
|
+
onCapture: (
|
|
31
|
+
uri: string,
|
|
32
|
+
videoPath: string | null,
|
|
33
|
+
/** Aspect (w÷h) of the box the preview rendered in — what the crop must match. */
|
|
34
|
+
viewAr: number,
|
|
35
|
+
) => void;
|
|
34
36
|
/** Card-guide aspect (width ÷ height): 1.586 for ID cards, 1.42 for passports. */
|
|
35
37
|
guideAspect?: number;
|
|
36
38
|
/** Capture is disabled while a previous capture/compress is in flight. */
|
|
37
39
|
busy?: boolean;
|
|
40
|
+
/** ISO-2 country + ID key — what auto-capture checks the document against. */
|
|
41
|
+
country?: string;
|
|
42
|
+
idType?: string;
|
|
43
|
+
/** Banked when a frame yields a check-digit-valid MRZ (the chip's key). */
|
|
44
|
+
onMrz?: (scan: MrzScan) => void;
|
|
45
|
+
/** An MRZ is already stored, so the gate need not hold out for one. */
|
|
46
|
+
mrzAlreadyCaptured?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Fill the parent instead of sitting in a 3:4 box.
|
|
49
|
+
*
|
|
50
|
+
* Used by immersive capture: the camera owns the whole screen, because a
|
|
51
|
+
* small preview is exactly what makes users hold a document too far away for
|
|
52
|
+
* the MRZ to resolve.
|
|
53
|
+
*/
|
|
54
|
+
fill?: boolean;
|
|
55
|
+
/** Drawn top-left in immersive mode, where the sheet's chrome is gone. */
|
|
56
|
+
onBack?: (() => void) | null;
|
|
57
|
+
/** The gallery escape, kept in immersive mode — it is the fallback when the
|
|
58
|
+
* camera simply will not read a worn or laminated document. */
|
|
59
|
+
onUpload?: (() => void) | null;
|
|
60
|
+
/** Safe-area bottom inset; the full-bleed camera runs under the indicator. */
|
|
61
|
+
bottomInset?: number;
|
|
38
62
|
}
|
|
39
63
|
|
|
40
64
|
export function CameraViewfinder({
|
|
@@ -44,9 +68,29 @@ export function CameraViewfinder({
|
|
|
44
68
|
onCapture,
|
|
45
69
|
guideAspect = 1.586,
|
|
46
70
|
busy = false,
|
|
71
|
+
country,
|
|
72
|
+
idType,
|
|
73
|
+
onMrz,
|
|
74
|
+
mrzAlreadyCaptured = false,
|
|
75
|
+
fill = false,
|
|
76
|
+
onBack,
|
|
77
|
+
onUpload,
|
|
78
|
+
bottomInset = 0,
|
|
47
79
|
}: CameraViewfinderProps): React.ReactElement {
|
|
48
80
|
const { colors } = useTheme();
|
|
49
|
-
|
|
81
|
+
// Prefer a back camera that CAN torch, then fall back to the library's
|
|
82
|
+
// default ranking. On multi-camera phones (seen live on a Galaxy S24) the
|
|
83
|
+
// default pick can land on a flashless back device — CameraX then rejects
|
|
84
|
+
// enableTorch with "No flash unit" — while a flash-capable back camera sits
|
|
85
|
+
// right next to it in the list. `hasTorch` is trustworthy on the logical
|
|
86
|
+
// device objects (it reads CameraX's hasFlashUnit()); only the
|
|
87
|
+
// physical-device wrappers stub it, and those lose this find() either way.
|
|
88
|
+
const allDevices = useCameraDevices();
|
|
89
|
+
const defaultBack = useCameraDevice('back');
|
|
90
|
+
const device = useMemo(
|
|
91
|
+
() => allDevices.find((d) => d.position === 'back' && d.hasTorch) ?? defaultBack,
|
|
92
|
+
[allDevices, defaultBack],
|
|
93
|
+
);
|
|
50
94
|
// v5: document-conservative still (OCR). Capture via the photo output, not a
|
|
51
95
|
// camera ref. Quality is prioritised so small text stays legible.
|
|
52
96
|
const photoOutput = usePhotoOutput({ qualityPrioritization: 'quality' });
|
|
@@ -75,82 +119,194 @@ export function CameraViewfinder({
|
|
|
75
119
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
76
120
|
}, [active, ready, side]);
|
|
77
121
|
|
|
78
|
-
const GH = GW / guideAspect;
|
|
79
|
-
const GY = (VB_H - GH) / 2;
|
|
80
122
|
|
|
81
|
-
const
|
|
82
|
-
|
|
123
|
+
const capturingRef = useRef(false);
|
|
124
|
+
const capture = useCallback(async () => {
|
|
125
|
+
if (busy || !ready || capturingRef.current) return;
|
|
126
|
+
// Guarded because auto-capture and the manual shutter can now both fire —
|
|
127
|
+
// two concurrent captures contend for the camera and one of them fails.
|
|
128
|
+
capturingRef.current = true;
|
|
83
129
|
try {
|
|
84
130
|
// Stop the video first (and grab its path) so the still and the clip don't
|
|
85
131
|
// contend for the camera, then capture the OCR still.
|
|
86
132
|
const videoPath = await recorder.stop();
|
|
87
133
|
const file = await photoOutput.capturePhotoToFile({ flashMode: 'off' }, {});
|
|
88
|
-
|
|
134
|
+
// The MEASURED box, via ref (state would be stale in this callback).
|
|
135
|
+
// Full-screen capture shows a much taller slice of the photo than the
|
|
136
|
+
// framed 3:4 box did; the crop must be told which one the user saw.
|
|
137
|
+
const b = boxRef.current;
|
|
138
|
+
const viewAr = b.width > 0 && b.height > 0 ? b.width / b.height : FRAMED_VIEW_AR;
|
|
139
|
+
onCapture(`file://${file.filePath}`, videoPath, viewAr);
|
|
89
140
|
} catch {
|
|
90
141
|
/* a failed shutter is a no-op — the user can tap again */
|
|
142
|
+
} finally {
|
|
143
|
+
capturingRef.current = false;
|
|
144
|
+
}
|
|
145
|
+
}, [busy, ready, recorder, photoOutput, onCapture]);
|
|
146
|
+
|
|
147
|
+
// ── Auto-capture ──────────────────────────────────────────────────────────
|
|
148
|
+
// Runs only with a document to check against; without one there is nothing to
|
|
149
|
+
// identify the frame as, and firing on "some text" would shoot anything.
|
|
150
|
+
const auto = useAutoCapture({
|
|
151
|
+
enabled: active && ready && !busy && !!country && !!idType,
|
|
152
|
+
country: country ?? '',
|
|
153
|
+
idType: idType ?? '',
|
|
154
|
+
// The back has no branding to identify itself by — the gate relaxes its
|
|
155
|
+
// identity and text-density demands there (see documentIdentity.ts).
|
|
156
|
+
side,
|
|
157
|
+
mrzAlreadyCaptured,
|
|
158
|
+
onMrz,
|
|
159
|
+
guideAspect,
|
|
160
|
+
// Front and back are the same idType; without this the gate stays latched
|
|
161
|
+
// from the front and the back never auto-captures.
|
|
162
|
+
resetKey: side,
|
|
163
|
+
onFire: () => void capture(),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Torch — a document under glass or in a dim room is the commonest reason a
|
|
167
|
+
// capture reads badly, and it is the one thing the user can fix instantly.
|
|
168
|
+
const [torch, setTorch] = useState(false);
|
|
169
|
+
const cameraRef = useRef<CameraRef>(null);
|
|
170
|
+
|
|
171
|
+
// IMPERATIVE, verified toggling — not the declarative `torchMode` prop.
|
|
172
|
+
// The prop path fires the controller call and drops the promise, so a
|
|
173
|
+
// rejection (CameraX refuses enableTorch when the bound camera reports no
|
|
174
|
+
// flash unit) leaves the button lit and the scene dark, with nothing in any
|
|
175
|
+
// log. Await the call, read the REAL state back off the controller (its
|
|
176
|
+
// getter reads CameraX TorchState, unlike the device object, whose
|
|
177
|
+
// `hasTorch` is a hardcoded stub for physical devices in this VisionCamera
|
|
178
|
+
// version), and keep UI state honest either way.
|
|
179
|
+
const toggleTorch = useCallback(async () => {
|
|
180
|
+
const controller = cameraRef.current?.controller;
|
|
181
|
+
if (!controller) return;
|
|
182
|
+
const next = !torch;
|
|
183
|
+
try {
|
|
184
|
+
await controller.setTorchMode(next ? 'on' : 'off');
|
|
185
|
+
const actual = controller.torchMode === 'on';
|
|
186
|
+
setTorch(actual);
|
|
187
|
+
if (actual !== next && __DEV__) {
|
|
188
|
+
console.warn('[kyc.camera] torch state did not follow the request');
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
if (__DEV__) console.warn('[kyc.camera] torch toggle failed:', err);
|
|
192
|
+
setTorch(false);
|
|
91
193
|
}
|
|
92
|
-
};
|
|
194
|
+
}, [torch]);
|
|
195
|
+
// Honest again: the device SELECTION above prefers a torch-capable camera,
|
|
196
|
+
// so when this is false there genuinely is no back camera with a flash unit
|
|
197
|
+
// and the button would be a lie. (An earlier fix trusted Android blindly,
|
|
198
|
+
// which produced a visible button driving a flashless camera — CameraX
|
|
199
|
+
// rejected every toggle with "No flash unit".)
|
|
200
|
+
const hasTorch = device?.hasTorch ?? false;
|
|
201
|
+
|
|
202
|
+
const hintText = documentHintText(auto.guidance.hint, documentLabel);
|
|
203
|
+
// The gate's live instruction once it has a document to judge; the static
|
|
204
|
+
// prompt until then (it needs a country + ID type to say anything useful).
|
|
205
|
+
const hint =
|
|
206
|
+
country && idType
|
|
207
|
+
? hintText
|
|
208
|
+
: `Align the ${side === 'back' ? 'BACK' : 'FRONT'} of your ${documentLabel}`;
|
|
209
|
+
const hintIsAction = documentHintIsAction(auto.guidance.hint);
|
|
210
|
+
|
|
211
|
+
// Immersive insets its own controls (there is no header to sit under).
|
|
212
|
+
//
|
|
213
|
+
// The guide is sized from the MEASURED container, not the window: on iOS the
|
|
214
|
+
// modal is a pageSheet, which is inset from the top of the screen, so window
|
|
215
|
+
// dimensions overshoot — the frame would sit low and the bottom bar would run
|
|
216
|
+
// off the edge. onLayout gives the box this actually renders into.
|
|
217
|
+
const [box, setBox] = useState({ width: 0, height: 0 });
|
|
218
|
+
const boxRef = useRef(box);
|
|
219
|
+
boxRef.current = box;
|
|
220
|
+
const topInset = Platform.OS === 'ios' ? 56 : 32;
|
|
93
221
|
|
|
94
222
|
return (
|
|
95
|
-
<View
|
|
96
|
-
{
|
|
97
|
-
|
|
223
|
+
<View
|
|
224
|
+
onLayout={
|
|
225
|
+
fill
|
|
226
|
+
? (e) => {
|
|
227
|
+
const { width, height } = e.nativeEvent.layout;
|
|
228
|
+
if (width !== box.width || height !== box.height) setBox({ width, height });
|
|
229
|
+
}
|
|
230
|
+
: undefined
|
|
231
|
+
}
|
|
232
|
+
style={
|
|
233
|
+
fill
|
|
234
|
+
? { flex: 1, overflow: 'hidden', backgroundColor: '#000000' }
|
|
235
|
+
: { width: '100%', aspectRatio: 3 / 4, borderRadius: radius.lg, overflow: 'hidden', backgroundColor: '#111111' }
|
|
236
|
+
}
|
|
237
|
+
>
|
|
238
|
+
{/* Mounted only once permission is GRANTED, not merely when a device
|
|
239
|
+
exists. VisionCamera configures its session on mount, and doing that
|
|
240
|
+
during the brief "requesting" window throws "Camera Permission not yet
|
|
241
|
+
granted!" as an uncaught rejection — `isActive={false}` does not
|
|
242
|
+
prevent the configuration. */}
|
|
243
|
+
{ready && active ? (
|
|
244
|
+
<Camera
|
|
245
|
+
ref={cameraRef}
|
|
246
|
+
style={{ flex: 1 }}
|
|
247
|
+
device={device}
|
|
248
|
+
isActive={active}
|
|
249
|
+
outputs={[photoOutput, videoOutput, auto.frameOutput]}
|
|
250
|
+
/>
|
|
98
251
|
) : (
|
|
99
252
|
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
|
100
253
|
<ActivityIndicator color="#FFFFFF" />
|
|
101
254
|
</View>
|
|
102
255
|
)}
|
|
103
256
|
|
|
104
|
-
{
|
|
105
|
-
|
|
257
|
+
{ready && active && fill && box.height > 0 ? (
|
|
258
|
+
<ImmersiveOverlay
|
|
259
|
+
side={side}
|
|
260
|
+
country={country ?? ''}
|
|
261
|
+
documentLabel={shortDocumentLabel(idType ?? null, documentLabel)}
|
|
262
|
+
guideAspect={guideAspect}
|
|
263
|
+
hint={hint}
|
|
264
|
+
hintIsAction={hintIsAction}
|
|
265
|
+
progress={auto.progress}
|
|
266
|
+
busy={busy}
|
|
267
|
+
torch={torch}
|
|
268
|
+
hasTorch={hasTorch}
|
|
269
|
+
onToggleTorch={() => void toggleTorch()}
|
|
270
|
+
onCapture={capture}
|
|
271
|
+
onBack={onBack ?? null}
|
|
272
|
+
onUpload={onUpload ?? null}
|
|
273
|
+
bottomInset={bottomInset}
|
|
274
|
+
width={box.width}
|
|
275
|
+
height={box.height}
|
|
276
|
+
topInset={topInset}
|
|
277
|
+
framing={auto.guidance.framing}
|
|
278
|
+
showMrzBand={idType === 'passport'}
|
|
279
|
+
/>
|
|
280
|
+
) : null}
|
|
281
|
+
|
|
282
|
+
{/* Framed (in-sheet) overlay */}
|
|
283
|
+
{ready && active && !fill ? (
|
|
106
284
|
<>
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
{`Align the ${side === 'back' ? 'BACK' : 'FRONT'} of your ${documentLabel}`}
|
|
125
|
-
</MyazaText>
|
|
126
|
-
</View>
|
|
127
|
-
</View>
|
|
285
|
+
<CardGuide guideAspect={guideAspect} primary={colors.primary} />
|
|
286
|
+
<HintBanner
|
|
287
|
+
fill={false}
|
|
288
|
+
isAction={hintIsAction}
|
|
289
|
+
text={hint}
|
|
290
|
+
/>
|
|
291
|
+
|
|
292
|
+
{hasTorch ? (
|
|
293
|
+
<OverlayButton
|
|
294
|
+
icon="lightbulb"
|
|
295
|
+
side="right"
|
|
296
|
+
fill={false}
|
|
297
|
+
active={torch}
|
|
298
|
+
label={torch ? 'Turn off the torch' : 'Turn on the torch'}
|
|
299
|
+
onPress={() => void toggleTorch()}
|
|
300
|
+
/>
|
|
301
|
+
) : null}
|
|
128
302
|
</>
|
|
129
303
|
) : null}
|
|
130
304
|
|
|
131
|
-
{/*
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
accessibilityRole="button"
|
|
137
|
-
accessibilityLabel="Capture photo"
|
|
138
|
-
style={({ pressed }) => ({
|
|
139
|
-
width: 64,
|
|
140
|
-
height: 64,
|
|
141
|
-
borderRadius: radius.full,
|
|
142
|
-
backgroundColor: ready ? '#FFFFFF' : colors.gray400,
|
|
143
|
-
borderWidth: 4,
|
|
144
|
-
borderColor: ready ? colors.primary : colors.gray300,
|
|
145
|
-
alignItems: 'center',
|
|
146
|
-
justifyContent: 'center',
|
|
147
|
-
opacity: busy ? 0.6 : 1,
|
|
148
|
-
transform: [{ scale: pressed ? 0.94 : 1 }],
|
|
149
|
-
})}
|
|
150
|
-
>
|
|
151
|
-
{busy ? <ActivityIndicator color={colors.primary} /> : <Icon name="camera" size={24} color={ready ? colors.primary : '#FFFFFF'} />}
|
|
152
|
-
</Pressable>
|
|
153
|
-
</View>
|
|
305
|
+
{/* Immersive draws its own shutter inside BottomBar (with the torch and
|
|
306
|
+
the upload escape), so only the framed variant needs this one. */}
|
|
307
|
+
{fill ? null : (
|
|
308
|
+
<Shutter onPress={capture} ready={ready} busy={busy} progress={auto.progress} />
|
|
309
|
+
)}
|
|
154
310
|
</View>
|
|
155
311
|
);
|
|
156
312
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
|
+
import { Pressable, TextInput, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius, spacing } from '../config/theme';
|
|
5
|
+
import { useTheme } from './runtime';
|
|
6
|
+
import { MyazaText } from './Typography';
|
|
7
|
+
import { MyazaInput } from './MyazaInput';
|
|
8
|
+
import type { OtpInputStyle } from '../types/workflow';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// The OTP code field.
|
|
12
|
+
//
|
|
13
|
+
// Which one renders — segmented boxes or a plain field — is the ORG's choice in
|
|
14
|
+
// the workflow builder (`inputStyle`), not the end user's.
|
|
15
|
+
//
|
|
16
|
+
// The segmented version is ONE hidden TextInput with boxes drawn over it, not
|
|
17
|
+
// N inputs with focus-juggling between them. That is what makes SMS autofill
|
|
18
|
+
// work (the platform fills a single field), makes paste work, and makes
|
|
19
|
+
// backspace behave — a per-box implementation breaks all three, and they are
|
|
20
|
+
// the three things people actually do with a code.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export function ContactCodeEntry({
|
|
24
|
+
code,
|
|
25
|
+
onChange,
|
|
26
|
+
codeLength,
|
|
27
|
+
style,
|
|
28
|
+
disabled,
|
|
29
|
+
onComplete,
|
|
30
|
+
}: {
|
|
31
|
+
code: string;
|
|
32
|
+
onChange: (code: string) => void;
|
|
33
|
+
codeLength: number;
|
|
34
|
+
/** Config-driven field style ('segmented' default). */
|
|
35
|
+
style: OtpInputStyle;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
/** Fires with the full code once the last slot fills (auto-submit). */
|
|
38
|
+
onComplete?: (code: string) => void;
|
|
39
|
+
}): React.ReactElement {
|
|
40
|
+
const handle = (raw: string): void => {
|
|
41
|
+
const next = raw.replace(/\D/g, '').slice(0, codeLength);
|
|
42
|
+
onChange(next);
|
|
43
|
+
if (next.length === codeLength) onComplete?.(next);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (style === 'text') {
|
|
47
|
+
return (
|
|
48
|
+
<MyazaInput
|
|
49
|
+
label="Verification code"
|
|
50
|
+
value={code}
|
|
51
|
+
onChangeText={handle}
|
|
52
|
+
placeholder={'0'.repeat(codeLength)}
|
|
53
|
+
keyboardType="number-pad"
|
|
54
|
+
maxLength={codeLength}
|
|
55
|
+
editable={!disabled}
|
|
56
|
+
autoFocus
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return <SegmentedCode code={code} onChangeText={handle} codeLength={codeLength} disabled={disabled} />;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function SegmentedCode({
|
|
65
|
+
code,
|
|
66
|
+
onChangeText,
|
|
67
|
+
codeLength,
|
|
68
|
+
disabled,
|
|
69
|
+
}: {
|
|
70
|
+
code: string;
|
|
71
|
+
onChangeText: (raw: string) => void;
|
|
72
|
+
codeLength: number;
|
|
73
|
+
disabled?: boolean;
|
|
74
|
+
}): React.ReactElement {
|
|
75
|
+
const { colors } = useTheme();
|
|
76
|
+
const inputRef = useRef<TextInput>(null);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<View>
|
|
80
|
+
{/* Boxes flex to fill the width at a fixed aspect ratio, and the FILLED
|
|
81
|
+
ones carry the primary border — same geometry and same signal as the
|
|
82
|
+
Flutter SDK's OtpInput, so the step reads identically on both. */}
|
|
83
|
+
<Pressable
|
|
84
|
+
onPress={() => inputRef.current?.focus()}
|
|
85
|
+
accessibilityRole="none"
|
|
86
|
+
style={{ flexDirection: 'row', gap: spacing.xs }}
|
|
87
|
+
>
|
|
88
|
+
{Array.from({ length: codeLength }, (_, i) => {
|
|
89
|
+
const filled = i < code.length;
|
|
90
|
+
return (
|
|
91
|
+
<View
|
|
92
|
+
key={i}
|
|
93
|
+
style={{
|
|
94
|
+
flex: 1,
|
|
95
|
+
aspectRatio: 0.82,
|
|
96
|
+
borderRadius: radius.md,
|
|
97
|
+
borderWidth: filled ? 1.5 : 1,
|
|
98
|
+
borderColor: filled ? colors.primary : colors.border,
|
|
99
|
+
backgroundColor: colors.background,
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
justifyContent: 'center',
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<MyazaText variant="heading2">{code[i] ?? ''}</MyazaText>
|
|
105
|
+
</View>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</Pressable>
|
|
109
|
+
|
|
110
|
+
{/* One real input behind the boxes: the platform's SMS autofill targets a
|
|
111
|
+
single field, and paste/backspace only behave against one value. It is
|
|
112
|
+
positioned over the boxes (not display:none) because a zero-size or
|
|
113
|
+
hidden input is skipped by autofill on both platforms. */}
|
|
114
|
+
<TextInput
|
|
115
|
+
ref={inputRef}
|
|
116
|
+
value={code}
|
|
117
|
+
onChangeText={onChangeText}
|
|
118
|
+
keyboardType="number-pad"
|
|
119
|
+
textContentType="oneTimeCode"
|
|
120
|
+
autoComplete="one-time-code"
|
|
121
|
+
maxLength={codeLength}
|
|
122
|
+
editable={!disabled}
|
|
123
|
+
autoFocus
|
|
124
|
+
caretHidden
|
|
125
|
+
accessibilityLabel="Verification code"
|
|
126
|
+
style={{
|
|
127
|
+
position: 'absolute',
|
|
128
|
+
top: 0,
|
|
129
|
+
left: 0,
|
|
130
|
+
right: 0,
|
|
131
|
+
bottom: 0,
|
|
132
|
+
opacity: 0.01,
|
|
133
|
+
color: 'transparent',
|
|
134
|
+
textAlign: 'center',
|
|
135
|
+
}}
|
|
136
|
+
/>
|
|
137
|
+
</View>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius, spacing } from '../config/theme';
|
|
5
|
+
import { useTheme } from './runtime';
|
|
6
|
+
import { MyazaText } from './Typography';
|
|
7
|
+
import { CountryFlag } from './CountryFlag';
|
|
8
|
+
import { DialCodePicker, type DialCodeOption } from './DialCodePicker';
|
|
9
|
+
import { Icon } from './Icon';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// A country select field: a MyazaSelect-styled collapsed trigger (flag + name
|
|
13
|
+
// + chevron) that opens THE country sheet — the phone field's dial-code picker
|
|
14
|
+
// minus the dial codes (keyboard-aware, autofocused search, results pinned
|
|
15
|
+
// above the keys). Used wherever a country is picked from a flat list (the
|
|
16
|
+
// key-person "where their ID was issued", the KYB "Country of registration"),
|
|
17
|
+
// so every country picker in the flow feels identical.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export function CountryField({
|
|
21
|
+
value,
|
|
22
|
+
options,
|
|
23
|
+
onChange,
|
|
24
|
+
placeholder = 'Select a country',
|
|
25
|
+
searchPlaceholder = 'Search country',
|
|
26
|
+
}: {
|
|
27
|
+
/** ISO-2 of the current pick; empty/null shows the placeholder. */
|
|
28
|
+
value: string | null;
|
|
29
|
+
/** The pickable countries — all ISO, or a workflow's registry subset. */
|
|
30
|
+
options: DialCodeOption[];
|
|
31
|
+
onChange: (code: string) => void;
|
|
32
|
+
placeholder?: string;
|
|
33
|
+
searchPlaceholder?: string;
|
|
34
|
+
}): React.ReactElement {
|
|
35
|
+
const { colors } = useTheme();
|
|
36
|
+
const [open, setOpen] = useState(false);
|
|
37
|
+
const name = value ? (options.find((o) => o.code === value)?.name ?? value) : null;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<Pressable
|
|
42
|
+
onPress={() => setOpen(true)}
|
|
43
|
+
accessibilityRole="button"
|
|
44
|
+
accessibilityLabel={name ?? placeholder}
|
|
45
|
+
style={{
|
|
46
|
+
flexDirection: 'row',
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
paddingHorizontal: spacing.md,
|
|
49
|
+
paddingVertical: spacing.md,
|
|
50
|
+
borderRadius: radius.sm,
|
|
51
|
+
borderWidth: 1,
|
|
52
|
+
borderColor: colors.border,
|
|
53
|
+
backgroundColor: colors.background,
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
{value ? (
|
|
57
|
+
<>
|
|
58
|
+
<CountryFlag country={value} size={20} />
|
|
59
|
+
<View style={{ width: spacing.sm }} />
|
|
60
|
+
</>
|
|
61
|
+
) : null}
|
|
62
|
+
<MyazaText
|
|
63
|
+
variant="body"
|
|
64
|
+
color={name ? undefined : colors.textMuted}
|
|
65
|
+
style={{ flex: 1 }}
|
|
66
|
+
numberOfLines={1}
|
|
67
|
+
>
|
|
68
|
+
{name ?? placeholder}
|
|
69
|
+
</MyazaText>
|
|
70
|
+
<View style={{ width: spacing.sm }} />
|
|
71
|
+
<Icon name="chevron-down" size={18} color={colors.textSecondary} />
|
|
72
|
+
</Pressable>
|
|
73
|
+
|
|
74
|
+
<DialCodePicker
|
|
75
|
+
visible={open}
|
|
76
|
+
options={options}
|
|
77
|
+
selected={value ?? ''}
|
|
78
|
+
searchPlaceholder={searchPlaceholder}
|
|
79
|
+
onPick={(code) => {
|
|
80
|
+
setOpen(false);
|
|
81
|
+
onChange(code);
|
|
82
|
+
}}
|
|
83
|
+
onClose={() => setOpen(false)}
|
|
84
|
+
/>
|
|
85
|
+
</>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -2,32 +2,32 @@ import React from 'react';
|
|
|
2
2
|
import { View } from 'react-native';
|
|
3
3
|
import { SvgXml } from 'react-native-svg';
|
|
4
4
|
// Raw flag SVG strings — the SAME source the web SDK uses (`country-flag-icons`),
|
|
5
|
-
// so the flags match.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import
|
|
5
|
+
// so the flags match. The WHOLE set is imported, not a curated subset: Global
|
|
6
|
+
// Documents let a workflow offer any ISO country, and a picker where some rows
|
|
7
|
+
// have a flag and others fall back to a grey box looks broken rather than
|
|
8
|
+
// incomplete. The strings are small and the bundler tree-shakes nothing here
|
|
9
|
+
// anyway, since the lookup is by runtime code.
|
|
10
|
+
import * as Flags from 'country-flag-icons/string/3x2';
|
|
11
11
|
|
|
12
12
|
import { radius } from '../config/theme';
|
|
13
|
-
import type { SupportedCountry } from '../types/config';
|
|
14
13
|
|
|
15
|
-
// Circular country flag
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
14
|
+
// Circular country flag — the RN mirror of the web SDK's `CountryFlag` and
|
|
15
|
+
// Flutter's `MyazaCountryFlag`. Renders the country-flag-icons 3:2 SVG via
|
|
16
|
+
// react-native-svg, cropped to a circle (cover-fit, like the web SDK's
|
|
17
|
+
// `object-cover` + `rounded-full`). Works identically on iOS and Android (no
|
|
18
|
+
// emoji-flag dependency, which Android does not render).
|
|
20
19
|
|
|
21
|
-
const FLAGS
|
|
20
|
+
const FLAGS = Flags as unknown as Record<string, string | undefined>;
|
|
22
21
|
|
|
23
22
|
export interface CountryFlagProps {
|
|
24
|
-
|
|
23
|
+
/** Any ISO-3166 alpha-2 code. */
|
|
24
|
+
country?: string | null;
|
|
25
25
|
size?: number;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export function CountryFlag({ country, size = 20 }: CountryFlagProps): React.ReactElement | null {
|
|
29
29
|
if (!country) return null;
|
|
30
|
-
const xml = FLAGS[country];
|
|
30
|
+
const xml = FLAGS[country.toUpperCase()];
|
|
31
31
|
if (!xml) return null;
|
|
32
32
|
|
|
33
33
|
// The flag is 3:2; to cover a square circle we render it wider than the circle
|