@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,107 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { MAX_VIDEO_BYTES } from '../../config/captureSettings';
|
|
4
|
+
import { compressVideo } from '../../services/mediaCompress';
|
|
5
|
+
import { withRetry } from '../../services/retry';
|
|
6
|
+
import { mapToKycError, safeReportError } from '../../services/errors';
|
|
7
|
+
import { useKyc, useKycConfig } from '../../components/runtime';
|
|
8
|
+
import { useToast } from '../../components/toast';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// The selfie and its liveness video, uploaded as soon as they exist.
|
|
12
|
+
//
|
|
13
|
+
// Split out of LivenessStep (200-line rule). It is a genuine boundary, not a
|
|
14
|
+
// slice for size: everything here is about getting two files to the server and
|
|
15
|
+
// reporting how that went, and none of it touches the camera or the state
|
|
16
|
+
// machine.
|
|
17
|
+
//
|
|
18
|
+
// The two uploads are deliberately NOT equal. The selfie is the verification —
|
|
19
|
+
// its failure is surfaced, retried and reported. The video is supporting
|
|
20
|
+
// evidence the server re-scores, so a failure there is swallowed: losing it must
|
|
21
|
+
// never block a user who has already done everything asked of them.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export interface SelfieUpload {
|
|
25
|
+
selfieUri: string | null;
|
|
26
|
+
setSelfieUri: (uri: string | null) => void;
|
|
27
|
+
uploading: boolean;
|
|
28
|
+
/** Which retry attempt is in flight, for the "retrying (2/3)" line. */
|
|
29
|
+
retryInfo: { attempt: number; total: number } | null;
|
|
30
|
+
uploadError: string | null;
|
|
31
|
+
setUploadError: (message: string | null) => void;
|
|
32
|
+
/** The uploaded selfie's media id — Continue is gated on it. */
|
|
33
|
+
selfieIdRef: React.MutableRefObject<string | null>;
|
|
34
|
+
/**
|
|
35
|
+
* The finished liveness-video path, kept so the review screen's "Try Again"
|
|
36
|
+
* can re-upload it. The recorder itself lives in useVideoRecorder.
|
|
37
|
+
*/
|
|
38
|
+
videoPathRef: React.MutableRefObject<string | null>;
|
|
39
|
+
uploadSelfieAndVideo: (selfie: string, videoPath: string | null) => Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function useSelfieUpload(): SelfieUpload {
|
|
43
|
+
const toast = useToast();
|
|
44
|
+
const config = useKycConfig();
|
|
45
|
+
const api = useKyc((s) => s.api);
|
|
46
|
+
const setMediaId = useKyc((s) => s.setMediaId);
|
|
47
|
+
|
|
48
|
+
const [selfieUri, setSelfieUri] = useState<string | null>(null);
|
|
49
|
+
const [uploading, setUploading] = useState(false);
|
|
50
|
+
const [retryInfo, setRetryInfo] = useState<{ attempt: number; total: number } | null>(null);
|
|
51
|
+
const [uploadError, setUploadError] = useState<string | null>(null);
|
|
52
|
+
const selfieIdRef = useRef<string | null>(null);
|
|
53
|
+
const videoPathRef = useRef<string | null>(null);
|
|
54
|
+
|
|
55
|
+
const uploadSelfieAndVideo = useCallback(
|
|
56
|
+
async (selfie: string, videoPath: string | null) => {
|
|
57
|
+
setUploading(true);
|
|
58
|
+
setUploadError(null);
|
|
59
|
+
setRetryInfo(null);
|
|
60
|
+
const onRetry = (attempt: number, total: number) => setRetryInfo({ attempt, total });
|
|
61
|
+
try {
|
|
62
|
+
const selfieId = await withRetry(
|
|
63
|
+
() => api.upload({ uri: selfie, type: 'image/jpeg' }, 'selfie'),
|
|
64
|
+
{ onRetry },
|
|
65
|
+
);
|
|
66
|
+
selfieIdRef.current = selfieId;
|
|
67
|
+
setMediaId('selfie', selfieId);
|
|
68
|
+
// Liveness video is best-effort — a failure here must not block the user.
|
|
69
|
+
if (videoPath) {
|
|
70
|
+
try {
|
|
71
|
+
// Transcode the raw recording down to a small evidence clip first.
|
|
72
|
+
const small = await compressVideo(videoPath);
|
|
73
|
+
const videoId = await withRetry(
|
|
74
|
+
() => api.upload({ uri: small, type: 'video/mp4' }, 'liveness_video', MAX_VIDEO_BYTES),
|
|
75
|
+
{ onRetry },
|
|
76
|
+
);
|
|
77
|
+
setMediaId('livenessVideo', videoId);
|
|
78
|
+
} catch {
|
|
79
|
+
/* keep the selfie, drop the video (incl. if it exceeded the 5MB cap) */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
setRetryInfo(null);
|
|
83
|
+
setUploading(false);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
setRetryInfo(null);
|
|
86
|
+
setUploading(false);
|
|
87
|
+
const kycError = mapToKycError(err, 'upload');
|
|
88
|
+
setUploadError(kycError.message);
|
|
89
|
+
toast.show({ variant: 'error', title: 'Upload failed', message: kycError.message });
|
|
90
|
+
safeReportError(config.onError, kycError);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[api, setMediaId, config.onError, toast],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
selfieUri,
|
|
98
|
+
setSelfieUri,
|
|
99
|
+
uploading,
|
|
100
|
+
retryInfo,
|
|
101
|
+
uploadError,
|
|
102
|
+
setUploadError,
|
|
103
|
+
selfieIdRef,
|
|
104
|
+
videoPathRef,
|
|
105
|
+
uploadSelfieAndVideo,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../../config/theme';
|
|
5
|
+
import { MyazaText } from '../../components/Typography';
|
|
6
|
+
import { MyazaButton } from '../../components/MyazaButton';
|
|
7
|
+
import { MrzScanView } from '../MrzScanView';
|
|
8
|
+
import type { MrzScan } from '../../mrz/parse';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The second chance at the chip's key.
|
|
12
|
+
*
|
|
13
|
+
* Without an MRZ there is nothing to unlock the chip with, and this step used
|
|
14
|
+
* to dead-end there — which made the chip unreachable whenever the document
|
|
15
|
+
* photo happened to read badly. Scanning the strip live costs the user one
|
|
16
|
+
* more aim of the camera and recovers the whole step.
|
|
17
|
+
*/
|
|
18
|
+
export function NfcMrzPrompt({
|
|
19
|
+
onScanned,
|
|
20
|
+
onSkip,
|
|
21
|
+
}: {
|
|
22
|
+
onScanned: (scan: MrzScan) => void;
|
|
23
|
+
/** Null hides the escape hatch (the flow set `allowSkip: false`). */
|
|
24
|
+
onSkip: (() => void) | null;
|
|
25
|
+
}): React.ReactElement {
|
|
26
|
+
return (
|
|
27
|
+
<View>
|
|
28
|
+
<MyazaText variant="bodyMedium">
|
|
29
|
+
Point your camera at the photo page of your document. We’ll read the
|
|
30
|
+
printed code that unlocks its chip.
|
|
31
|
+
</MyazaText>
|
|
32
|
+
<View style={{ height: spacing.md }} />
|
|
33
|
+
<MrzScanView onScanned={onScanned} />
|
|
34
|
+
<View style={{ height: spacing.md }} />
|
|
35
|
+
{onSkip ? (
|
|
36
|
+
<MyazaButton label="Continue without the chip" variant="ghost" onPress={onSkip} />
|
|
37
|
+
) : null}
|
|
38
|
+
</View>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../../config/theme';
|
|
5
|
+
import { useTheme } from '../../components/runtime';
|
|
6
|
+
import { MyazaText } from '../../components/Typography';
|
|
7
|
+
import { NfcReadProgress } from './NfcReadProgress';
|
|
8
|
+
import { NfcScannedSummary } from './NfcScannedSummary';
|
|
9
|
+
import { NfcScanIllustration } from './NfcScanIllustration';
|
|
10
|
+
import type { NfcReadStage } from '../../emrtd';
|
|
11
|
+
import type { MrzScan } from '../../mrz/parse';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The chip step's body: the document illustration, the read narration, and
|
|
15
|
+
* what the MRZ scan found.
|
|
16
|
+
*
|
|
17
|
+
* The narration is the part that matters. iOS gets a system sheet describing
|
|
18
|
+
* the read for free; Android gets nothing, so without this the same read is a
|
|
19
|
+
* silent spinner — and silence is exactly what makes a user lift the document,
|
|
20
|
+
* which aborts it.
|
|
21
|
+
*/
|
|
22
|
+
export function NfcReadBody({
|
|
23
|
+
reading,
|
|
24
|
+
stage,
|
|
25
|
+
scan,
|
|
26
|
+
}: {
|
|
27
|
+
reading: boolean;
|
|
28
|
+
stage: NfcReadStage;
|
|
29
|
+
scan: MrzScan;
|
|
30
|
+
}): React.ReactElement {
|
|
31
|
+
const { colors } = useTheme();
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
<NfcScanIllustration />
|
|
35
|
+
<View style={{ height: spacing.lg }} />
|
|
36
|
+
|
|
37
|
+
{reading ? (
|
|
38
|
+
<>
|
|
39
|
+
<MyazaText variant="bodyMedium" style={{ textAlign: 'center' }}>
|
|
40
|
+
{stage === 'waiting'
|
|
41
|
+
? 'Hold the top edge of your document flat against the back of your phone and keep still…'
|
|
42
|
+
: 'Chip detected — keep the document still until this finishes.'}
|
|
43
|
+
</MyazaText>
|
|
44
|
+
<View style={{ height: spacing.md }} />
|
|
45
|
+
<NfcReadProgress stage={stage} />
|
|
46
|
+
</>
|
|
47
|
+
) : (
|
|
48
|
+
<>
|
|
49
|
+
<MyazaText variant="bodySmall" color={colors.textMuted} style={{ textAlign: 'center' }}>
|
|
50
|
+
Hold the top edge of your document flat against the back of your
|
|
51
|
+
phone and keep still…
|
|
52
|
+
</MyazaText>
|
|
53
|
+
{/* Shown BEFORE the read, so the wrong document is obvious now rather
|
|
54
|
+
than after an opaque BAC failure. */}
|
|
55
|
+
<View style={{ height: spacing.md }} />
|
|
56
|
+
<View style={{ alignSelf: 'stretch' }}>
|
|
57
|
+
<NfcScannedSummary scan={scan} />
|
|
58
|
+
</View>
|
|
59
|
+
</>
|
|
60
|
+
)}
|
|
61
|
+
</>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ActivityIndicator, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../../config/theme';
|
|
5
|
+
import { useTheme } from '../../components/runtime';
|
|
6
|
+
import { MyazaText } from '../../components/Typography';
|
|
7
|
+
import { Icon } from '../../components/Icon';
|
|
8
|
+
import { NFC_STAGE_ORDER, nfcStageLabel, type NfcReadStage } from '../../emrtd';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// NFC read progress.
|
|
12
|
+
//
|
|
13
|
+
// iOS puts a system sheet over the screen for the whole chip read and narrates
|
|
14
|
+
// it. Android has NO system NFC UI whatsoever, so the same read is otherwise a
|
|
15
|
+
// bare spinner: the user cannot tell whether the chip has even been detected,
|
|
16
|
+
// let alone that three data groups are being pulled off it — and the natural
|
|
17
|
+
// response to that silence is to lift the document, which aborts the read.
|
|
18
|
+
//
|
|
19
|
+
// This draws the narration the platform does not. Same wording as the iOS
|
|
20
|
+
// sheet, so both platforms describe the read identically. Ported from the
|
|
21
|
+
// Flutter SDK's NfcReadProgress.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The steps worth showing. `waiting` is NOT one of them — it is the state
|
|
26
|
+
* before any step starts, and it gets its own prominent line above.
|
|
27
|
+
*/
|
|
28
|
+
const STEPS: NfcReadStage[] = [
|
|
29
|
+
'authenticating',
|
|
30
|
+
'readingData',
|
|
31
|
+
'readingSecurity',
|
|
32
|
+
'readingPhoto',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export function NfcReadProgress({ stage }: { stage: NfcReadStage }): React.ReactElement {
|
|
36
|
+
const { colors } = useTheme();
|
|
37
|
+
|
|
38
|
+
// Waiting is the state users misread as "nothing is happening", so it says
|
|
39
|
+
// what the phone is doing rather than leaving a silent spinner.
|
|
40
|
+
if (stage === 'waiting') {
|
|
41
|
+
return (
|
|
42
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
|
|
43
|
+
<ActivityIndicator size="small" color={colors.primary} />
|
|
44
|
+
<View style={{ width: 8 }} />
|
|
45
|
+
<MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>
|
|
46
|
+
{nfcStageLabel('waiting')}
|
|
47
|
+
</MyazaText>
|
|
48
|
+
</View>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const currentIndex = NFC_STAGE_ORDER.indexOf(stage);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<View style={{ alignSelf: 'stretch' }}>
|
|
56
|
+
{STEPS.map((step) => {
|
|
57
|
+
// `done` completes EVERY step: the photo group is skipped when the
|
|
58
|
+
// security object didn't come back, and a step left spinning after a
|
|
59
|
+
// successful read would read as a failure.
|
|
60
|
+
const complete = stage === 'done' || currentIndex > NFC_STAGE_ORDER.indexOf(step);
|
|
61
|
+
const active = stage === step;
|
|
62
|
+
return (
|
|
63
|
+
<View
|
|
64
|
+
key={step}
|
|
65
|
+
style={{ flexDirection: 'row', alignItems: 'center', marginBottom: spacing.sm }}
|
|
66
|
+
>
|
|
67
|
+
<View style={{ width: 18, alignItems: 'center' }}>
|
|
68
|
+
{complete ? (
|
|
69
|
+
<Icon name="badge-check" size={16} color={colors.success} />
|
|
70
|
+
) : active ? (
|
|
71
|
+
<ActivityIndicator size="small" color={colors.primary} />
|
|
72
|
+
) : (
|
|
73
|
+
<View
|
|
74
|
+
style={{
|
|
75
|
+
width: 8,
|
|
76
|
+
height: 8,
|
|
77
|
+
borderRadius: 4,
|
|
78
|
+
backgroundColor: colors.gray300,
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
</View>
|
|
83
|
+
<View style={{ width: 10 }} />
|
|
84
|
+
<MyazaText
|
|
85
|
+
variant="bodySmall"
|
|
86
|
+
color={complete || active ? colors.textDark : colors.textMuted}
|
|
87
|
+
style={{ fontWeight: active ? '600' : '400', flex: 1 }}
|
|
88
|
+
>
|
|
89
|
+
{nfcStageLabel(step)}
|
|
90
|
+
</MyazaText>
|
|
91
|
+
</View>
|
|
92
|
+
);
|
|
93
|
+
})}
|
|
94
|
+
</View>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { AccessibilityInfo, Animated, Easing, View } from 'react-native';
|
|
3
|
+
import Svg, { Circle, Defs, G, Path, RadialGradient, Rect, Stop } from 'react-native-svg';
|
|
4
|
+
|
|
5
|
+
import { useTheme } from '../../components/runtime';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The NFC scan illustration, ported 1:1 from the web SDK's
|
|
9
|
+
* `NfcScanIllustration` — same 320×240 geometry, same three beats: a document
|
|
10
|
+
* with a CHIP, a phone held over it, a FIELD between them. The web component
|
|
11
|
+
* is the design source of truth; every coordinate here matches it digit for
|
|
12
|
+
* digit so the builder preview (web) and the live mobile screen stay the same
|
|
13
|
+
* picture. Change the web one first, then mirror.
|
|
14
|
+
*
|
|
15
|
+
* Exactly ONE element is `primary`: the field. Everything else is neutral
|
|
16
|
+
* theme tokens, so it inherits an org's palette and works in both modes.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Where the field originates: on the phone's edge, at antenna height. */
|
|
20
|
+
const COUPLING = { x: 173, y: 88 } as const;
|
|
21
|
+
|
|
22
|
+
/** Arc radii, innermost first — each is one "ripple" of the field. */
|
|
23
|
+
const WAVES = [
|
|
24
|
+
{ r: 16, opacity: 0.9, delay: 0 },
|
|
25
|
+
{ r: 28, opacity: 0.65, delay: 150 },
|
|
26
|
+
{ r: 40, opacity: 0.42, delay: 300 },
|
|
27
|
+
{ r: 50, opacity: 0.25, delay: 450 },
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
/** Half-angle of the fan, in degrees. */
|
|
31
|
+
const SPREAD = 56;
|
|
32
|
+
|
|
33
|
+
/** Web keyframes: 0% .25 → 45% 1 → 100% .25 over 1.8s, staggered. */
|
|
34
|
+
const PERIOD_MS = 1800;
|
|
35
|
+
const RISE_MS = PERIOD_MS * 0.45;
|
|
36
|
+
|
|
37
|
+
function wavePath(r: number): string {
|
|
38
|
+
const rad = (SPREAD * Math.PI) / 180;
|
|
39
|
+
const dx = Math.cos(rad) * r;
|
|
40
|
+
const dy = Math.sin(rad) * r;
|
|
41
|
+
const x = COUPLING.x + dx;
|
|
42
|
+
return `M ${x.toFixed(1)} ${(COUPLING.y - dy).toFixed(1)} A ${r} ${r} 0 0 1 ${x.toFixed(1)} ${(COUPLING.y + dy).toFixed(1)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Web-parity alpha suffixes for the theme's hex colours (the web draws with
|
|
49
|
+
* `token/NN` Tailwind opacities; RN colours are plain hex strings).
|
|
50
|
+
*/
|
|
51
|
+
const A10 = '1A';
|
|
52
|
+
const A20 = '33';
|
|
53
|
+
const A25 = '40';
|
|
54
|
+
const A40 = '66';
|
|
55
|
+
const A50 = '80';
|
|
56
|
+
const A70 = 'B3';
|
|
57
|
+
|
|
58
|
+
export function NfcScanIllustration(): React.ReactElement {
|
|
59
|
+
const { colors } = useTheme();
|
|
60
|
+
|
|
61
|
+
// One value per ripple; the element opacity animates while strokeOpacity
|
|
62
|
+
// holds the per-arc base — exactly the web's two-layer opacity.
|
|
63
|
+
const pulses = useRef(WAVES.map(() => new Animated.Value(1))).current;
|
|
64
|
+
const [reduceMotion, setReduceMotion] = useState(false);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
let cancelled = false;
|
|
68
|
+
void AccessibilityInfo.isReduceMotionEnabled().then((reduced) => {
|
|
69
|
+
if (!cancelled) setReduceMotion(reduced);
|
|
70
|
+
});
|
|
71
|
+
return () => {
|
|
72
|
+
cancelled = true;
|
|
73
|
+
};
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (reduceMotion) {
|
|
78
|
+
// Same behaviour as the web's reduced-motion rule: the animation stops
|
|
79
|
+
// and the arcs stay VISIBLE at full element opacity.
|
|
80
|
+
pulses.forEach((v) => v.setValue(1));
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const runs = pulses.map((value, i) => {
|
|
84
|
+
value.setValue(0.25);
|
|
85
|
+
const run = Animated.sequence([
|
|
86
|
+
Animated.delay(WAVES[i]!.delay),
|
|
87
|
+
Animated.loop(
|
|
88
|
+
Animated.sequence([
|
|
89
|
+
Animated.timing(value, {
|
|
90
|
+
toValue: 1,
|
|
91
|
+
duration: RISE_MS,
|
|
92
|
+
easing: Easing.inOut(Easing.ease),
|
|
93
|
+
// SVG props can't ride the native driver.
|
|
94
|
+
useNativeDriver: false,
|
|
95
|
+
}),
|
|
96
|
+
Animated.timing(value, {
|
|
97
|
+
toValue: 0.25,
|
|
98
|
+
duration: PERIOD_MS - RISE_MS,
|
|
99
|
+
easing: Easing.inOut(Easing.ease),
|
|
100
|
+
useNativeDriver: false,
|
|
101
|
+
}),
|
|
102
|
+
]),
|
|
103
|
+
),
|
|
104
|
+
]);
|
|
105
|
+
run.start();
|
|
106
|
+
return run;
|
|
107
|
+
});
|
|
108
|
+
return () => runs.forEach((r) => r.stop());
|
|
109
|
+
}, [pulses, reduceMotion]);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
// Same footprint as the web: full width up to 384 (`max-w-sm`), 320:240.
|
|
113
|
+
<View style={{ width: '100%', maxWidth: 384, aspectRatio: 320 / 240, alignSelf: 'center' }}>
|
|
114
|
+
<Svg width="100%" height="100%" viewBox="0 0 320 240">
|
|
115
|
+
<Defs>
|
|
116
|
+
{/* The field falls off with distance — the reason the document has
|
|
117
|
+
to be held CLOSE, said as a gradient instead of a caption. */}
|
|
118
|
+
<RadialGradient
|
|
119
|
+
id="kyc-nfc-field"
|
|
120
|
+
cx={COUPLING.x}
|
|
121
|
+
cy={COUPLING.y}
|
|
122
|
+
r={90}
|
|
123
|
+
gradientUnits="userSpaceOnUse"
|
|
124
|
+
>
|
|
125
|
+
<Stop offset="0" stopColor={colors.primary} stopOpacity={0.18} />
|
|
126
|
+
<Stop offset="0.55" stopColor={colors.primary} stopOpacity={0.06} />
|
|
127
|
+
<Stop offset="1" stopColor={colors.primary} stopOpacity={0} />
|
|
128
|
+
</RadialGradient>
|
|
129
|
+
</Defs>
|
|
130
|
+
|
|
131
|
+
{/* ── The document, tucked BEHIND the phone: portrait, chip, MRZ. ── */}
|
|
132
|
+
<G rotation={7} origin="212, 112">
|
|
133
|
+
<Rect
|
|
134
|
+
x={154}
|
|
135
|
+
y={30}
|
|
136
|
+
width={114}
|
|
137
|
+
height={164}
|
|
138
|
+
rx={10}
|
|
139
|
+
fill={`${colors.backgroundSecondary}${A50}`}
|
|
140
|
+
stroke={colors.border}
|
|
141
|
+
strokeWidth={2}
|
|
142
|
+
/>
|
|
143
|
+
|
|
144
|
+
{/* Portrait, top-right — an outline glyph floating in its frame. */}
|
|
145
|
+
<Rect
|
|
146
|
+
x={224}
|
|
147
|
+
y={46}
|
|
148
|
+
width={32}
|
|
149
|
+
height={42}
|
|
150
|
+
rx={6}
|
|
151
|
+
fill={`${colors.textDark}${A10}`}
|
|
152
|
+
stroke={colors.border}
|
|
153
|
+
strokeWidth={1.5}
|
|
154
|
+
/>
|
|
155
|
+
<G
|
|
156
|
+
stroke={`${colors.textSecondary}${A50}`}
|
|
157
|
+
strokeWidth={2}
|
|
158
|
+
strokeLinecap="round"
|
|
159
|
+
fill="none"
|
|
160
|
+
>
|
|
161
|
+
<Circle cx={240} cy={61} r={5.5} />
|
|
162
|
+
<Path d="M231.5 77.5a8.5 8.5 0 0 1 17 0" />
|
|
163
|
+
</G>
|
|
164
|
+
|
|
165
|
+
{/* The chip — the familiar card contact pad, centred on the card,
|
|
166
|
+
below the fan's lowest reach so the arcs can never touch it. */}
|
|
167
|
+
<G stroke={`${colors.textSecondary}${A70}`} strokeWidth={1.5} fill="none">
|
|
168
|
+
<Rect
|
|
169
|
+
x={200.5}
|
|
170
|
+
y={133}
|
|
171
|
+
width={21}
|
|
172
|
+
height={16}
|
|
173
|
+
rx={3}
|
|
174
|
+
fill={`${colors.textDark}${A10}`}
|
|
175
|
+
/>
|
|
176
|
+
<Path d="M200.5 138.3h21M200.5 143.7h21M211 138.3v5.4" />
|
|
177
|
+
</G>
|
|
178
|
+
|
|
179
|
+
{/* The MRZ, low on the card, running edge to edge behind the phone.
|
|
180
|
+
Border tone: background texture, not an icon. */}
|
|
181
|
+
<G stroke={colors.border} strokeWidth={3} strokeLinecap="round" fill="none">
|
|
182
|
+
<Path d="M170 174h84" strokeDasharray="6 4" />
|
|
183
|
+
<Path d="M166 184h88" strokeDasharray="4 5" strokeOpacity={0.7} />
|
|
184
|
+
</G>
|
|
185
|
+
</G>
|
|
186
|
+
|
|
187
|
+
{/* ── The phone, in front, screen toward us — pressed in close. ── */}
|
|
188
|
+
<G x={12}>
|
|
189
|
+
<G rotation={-5} origin="112, 126">
|
|
190
|
+
{/* Volume buttons, on the side away from the field. */}
|
|
191
|
+
<Rect x={57} y={84} width={3} height={14} rx={1.5} fill={`${colors.textDark}${A25}`} />
|
|
192
|
+
<Rect x={57} y={103} width={3} height={14} rx={1.5} fill={`${colors.textDark}${A25}`} />
|
|
193
|
+
|
|
194
|
+
{/* Opaque base + tint: the tint token alone is an alpha fill and
|
|
195
|
+
let the card ghost THROUGH the glass. */}
|
|
196
|
+
<Rect x={60} y={30} width={104} height={192} rx={24} fill={colors.background} />
|
|
197
|
+
<Rect
|
|
198
|
+
x={60}
|
|
199
|
+
y={30}
|
|
200
|
+
width={104}
|
|
201
|
+
height={192}
|
|
202
|
+
rx={24}
|
|
203
|
+
fill={`${colors.backgroundSecondary}${A40}`}
|
|
204
|
+
stroke={`${colors.textDark}${A25}`}
|
|
205
|
+
strokeWidth={2}
|
|
206
|
+
/>
|
|
207
|
+
<Rect x={99} y={46} width={26} height={8} rx={4} fill={`${colors.textDark}${A25}`} />
|
|
208
|
+
|
|
209
|
+
{/* Ghost caption bars where the instruction copy sits in the app. */}
|
|
210
|
+
<Rect x={87} y={170} width={50} height={5} rx={2.5} fill={colors.border} />
|
|
211
|
+
<Rect
|
|
212
|
+
x={96}
|
|
213
|
+
y={181}
|
|
214
|
+
width={32}
|
|
215
|
+
height={4}
|
|
216
|
+
rx={2}
|
|
217
|
+
fill={colors.border}
|
|
218
|
+
opacity={0.6}
|
|
219
|
+
/>
|
|
220
|
+
|
|
221
|
+
{/* Home indicator — the strongest "this is the front" cue. */}
|
|
222
|
+
<Rect x={97} y={205} width={30} height={3.5} rx={1.75} fill={`${colors.textDark}${A20}`} />
|
|
223
|
+
</G>
|
|
224
|
+
</G>
|
|
225
|
+
|
|
226
|
+
{/* ── The field, over both objects. ── */}
|
|
227
|
+
<Circle cx={COUPLING.x} cy={COUPLING.y} r={90} fill="url(#kyc-nfc-field)" />
|
|
228
|
+
<Circle cx={COUPLING.x} cy={COUPLING.y} r={4.5} fill={colors.primary} />
|
|
229
|
+
{WAVES.map((w, i) => (
|
|
230
|
+
<AnimatedPath
|
|
231
|
+
key={w.r}
|
|
232
|
+
d={wavePath(w.r)}
|
|
233
|
+
stroke={colors.primary}
|
|
234
|
+
strokeWidth={3.25}
|
|
235
|
+
strokeLinecap="round"
|
|
236
|
+
fill="none"
|
|
237
|
+
strokeOpacity={w.opacity}
|
|
238
|
+
opacity={pulses[i]!}
|
|
239
|
+
/>
|
|
240
|
+
))}
|
|
241
|
+
</Svg>
|
|
242
|
+
</View>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius, spacing } from '../../config/theme';
|
|
5
|
+
import { useTheme } from '../../components/runtime';
|
|
6
|
+
import { MyazaText } from '../../components/Typography';
|
|
7
|
+
import { Icon } from '../../components/Icon';
|
|
8
|
+
import type { MrzScan } from '../../mrz/parse';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Scanned identity card.
|
|
12
|
+
//
|
|
13
|
+
// What the MRZ scan read, shown back for confirmation — so the wrong document is
|
|
14
|
+
// obvious BEFORE the chip read rather than after an opaque BAC failure.
|
|
15
|
+
//
|
|
16
|
+
// NAME and DOCUMENT NUMBER only, 1:1 with the Flutter SDK's NfcScannedSummary.
|
|
17
|
+
// Those two are what a user actually checks against the printed page; the dates
|
|
18
|
+
// were noise on a screen whose job is "is this the right document, yes or no".
|
|
19
|
+
//
|
|
20
|
+
// Labels are uppercase, 10pt, 0.8 letter-spacing — the same treatment Flutter
|
|
21
|
+
// gives them, so the two SDKs read as one product.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export function NfcScannedSummary({ scan }: { scan: MrzScan }): React.ReactElement {
|
|
25
|
+
const { colors } = useTheme();
|
|
26
|
+
const name = [scan.firstName, scan.lastName].filter(Boolean).join(' ');
|
|
27
|
+
|
|
28
|
+
const Label = ({ children }: { children: string }): React.ReactElement => (
|
|
29
|
+
<MyazaText
|
|
30
|
+
variant="bodySmall"
|
|
31
|
+
color={colors.textMuted}
|
|
32
|
+
style={{ fontSize: 10, fontWeight: '600', letterSpacing: 0.8 }}
|
|
33
|
+
>
|
|
34
|
+
{children.toUpperCase()}
|
|
35
|
+
</MyazaText>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View
|
|
40
|
+
style={{
|
|
41
|
+
padding: spacing.md,
|
|
42
|
+
backgroundColor: colors.primary50,
|
|
43
|
+
borderRadius: radius.md,
|
|
44
|
+
borderWidth: 1,
|
|
45
|
+
borderColor: `${colors.primary}40`,
|
|
46
|
+
flexDirection: 'row',
|
|
47
|
+
alignItems: 'flex-start',
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{/* Optically aligned with the first line of text rather than the block's
|
|
51
|
+
centre, which drifts as the name wraps. */}
|
|
52
|
+
<View style={{ paddingTop: 2 }}>
|
|
53
|
+
<Icon name="badge-check" size={18} color={colors.primary} />
|
|
54
|
+
</View>
|
|
55
|
+
<View style={{ width: spacing.sm }} />
|
|
56
|
+
<View style={{ flex: 1 }}>
|
|
57
|
+
{name ? (
|
|
58
|
+
<View style={{ marginBottom: spacing.sm }}>
|
|
59
|
+
<Label>Name</Label>
|
|
60
|
+
<MyazaText variant="bodyMedium" color={colors.textDark} style={{ fontWeight: '600' }}>
|
|
61
|
+
{name}
|
|
62
|
+
</MyazaText>
|
|
63
|
+
</View>
|
|
64
|
+
) : null}
|
|
65
|
+
<View>
|
|
66
|
+
<Label>Document number</Label>
|
|
67
|
+
<MyazaText variant="bodyMedium" color={colors.textDark} style={{ fontWeight: '600' }}>
|
|
68
|
+
{scan.documentNumber}
|
|
69
|
+
</MyazaText>
|
|
70
|
+
</View>
|
|
71
|
+
</View>
|
|
72
|
+
</View>
|
|
73
|
+
);
|
|
74
|
+
}
|