@myazahq/kyc-sdk-react-native 2.0.1 → 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/CameraPermissionView.tsx +39 -3
- 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 +98 -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 +190 -145
- package/src/screens/IdInputStep.tsx +19 -9
- package/src/screens/IdTypeStep.tsx +74 -33
- package/src/screens/KeyPeopleInviteLinks.tsx +239 -0
- package/src/screens/LivenessStep.tsx +181 -267
- 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 +3 -2
- package/src/services/validators.ts +3 -0
- 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 -119
- 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,200 @@
|
|
|
1
|
+
import { fromBase64, toBase64 } from './bytes';
|
|
2
|
+
import { buildBacChallenge, completeBac } from './bac';
|
|
3
|
+
import { primitivesFromNative, type EmrtdPrimitives, type MrzKeyFields } from './crypto';
|
|
4
|
+
import { EF, readFile, type Transceive } from './files';
|
|
5
|
+
import { readOptionalFile } from './optionalRead';
|
|
6
|
+
import { SecureMessagingSession } from './secureMessaging';
|
|
7
|
+
import type { NfcReadStage } from './stages';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Reading a chip, end to end.
|
|
11
|
+
//
|
|
12
|
+
// Select the application, run BAC, then read the files. What comes back is
|
|
13
|
+
// handed to the SERVER, which is where authenticity is decided: the server
|
|
14
|
+
// hashes DG1 against the signed security object and checks the document
|
|
15
|
+
// signer's signature. Nothing here may conclude that a chip is genuine — a
|
|
16
|
+
// client-side "authentic" flag is a claim an attacker controls.
|
|
17
|
+
//
|
|
18
|
+
// So the job of this file is narrow and honest: get the bytes off the chip, and
|
|
19
|
+
// report clearly when it could not.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/** The eMRTD application identifier — selected before anything else works. */
|
|
23
|
+
const AID = new Uint8Array([0xa0, 0x00, 0x00, 0x02, 0x47, 0x10, 0x01]);
|
|
24
|
+
|
|
25
|
+
const SW_OK = 0x9000;
|
|
26
|
+
|
|
27
|
+
export class EmrtdSessionError extends Error {
|
|
28
|
+
constructor(
|
|
29
|
+
message: string,
|
|
30
|
+
/** A stable token the UI branches on, rather than matching prose. */
|
|
31
|
+
readonly code: 'select_failed' | 'bac_failed' | 'read_failed' | 'transport',
|
|
32
|
+
) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = 'EmrtdSessionError';
|
|
35
|
+
Object.setPrototypeOf(this, EmrtdSessionError.prototype);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** What a successful read yields, all base64 for the submission. */
|
|
40
|
+
export interface EmrtdReadResult {
|
|
41
|
+
/** DG1 — the MRZ as the chip stores it. Always present on success. */
|
|
42
|
+
dg1: string;
|
|
43
|
+
/** EF.SOD — the signed security object. The server needs it to verify DG1. */
|
|
44
|
+
sod?: string;
|
|
45
|
+
/** DG2 — the portrait. Best-effort; the largest file and the likeliest to drop. */
|
|
46
|
+
dg2?: string;
|
|
47
|
+
/** How the chip was unlocked. */
|
|
48
|
+
chipAuth: 'bac';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The raw transport, before secure messaging wraps it. */
|
|
52
|
+
export interface EmrtdTransport {
|
|
53
|
+
/** Send an APDU, get data + status word back. */
|
|
54
|
+
transceive(apduBase64: string): Promise<{ data: string; statusWord: number }>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** SELECT the eMRTD application. Nothing else responds until this succeeds. */
|
|
58
|
+
async function selectApplication(transport: EmrtdTransport): Promise<void> {
|
|
59
|
+
const apdu = new Uint8Array([0x00, 0xa4, 0x04, 0x0c, AID.length, ...AID]);
|
|
60
|
+
const { statusWord } = await transport.transceive(toBase64(apdu));
|
|
61
|
+
if (statusWord !== SW_OK) {
|
|
62
|
+
throw new EmrtdSessionError(
|
|
63
|
+
`The chip did not accept the passport application (0x${statusWord.toString(16)}).`,
|
|
64
|
+
'select_failed',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** GET CHALLENGE — the chip's 8-byte nonce, which starts BAC. */
|
|
70
|
+
async function getChallenge(transport: EmrtdTransport): Promise<Uint8Array> {
|
|
71
|
+
const { data, statusWord } = await transport.transceive(
|
|
72
|
+
toBase64(new Uint8Array([0x00, 0x84, 0x00, 0x00, 0x08])),
|
|
73
|
+
);
|
|
74
|
+
if (statusWord !== SW_OK) {
|
|
75
|
+
throw new EmrtdSessionError(
|
|
76
|
+
`The chip refused to issue a challenge (0x${statusWord.toString(16)}).`,
|
|
77
|
+
'bac_failed',
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return fromBase64(data);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Run BAC and open a secure-messaging session.
|
|
85
|
+
*
|
|
86
|
+
* A failure here means the MRZ was read wrong, or the thing answering is not
|
|
87
|
+
* the document — NOT that the read should be retried with the same key.
|
|
88
|
+
*/
|
|
89
|
+
async function openSession(
|
|
90
|
+
p: EmrtdPrimitives,
|
|
91
|
+
transport: EmrtdTransport,
|
|
92
|
+
mrz: MrzKeyFields,
|
|
93
|
+
): Promise<SecureMessagingSession> {
|
|
94
|
+
const rndIc = await getChallenge(transport);
|
|
95
|
+
const challenge = buildBacChallenge(p, mrz, rndIc);
|
|
96
|
+
|
|
97
|
+
const apdu = new Uint8Array([
|
|
98
|
+
0x00,
|
|
99
|
+
0x82,
|
|
100
|
+
0x00,
|
|
101
|
+
0x00,
|
|
102
|
+
challenge.command.length,
|
|
103
|
+
...challenge.command,
|
|
104
|
+
0x28,
|
|
105
|
+
]);
|
|
106
|
+
const { data, statusWord } = await transport.transceive(toBase64(apdu));
|
|
107
|
+
if (statusWord !== SW_OK) {
|
|
108
|
+
throw new EmrtdSessionError(
|
|
109
|
+
'The chip rejected the key derived from the document. Check the scanned details.',
|
|
110
|
+
'bac_failed',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { keys, ssc } = completeBac(p, challenge, fromBase64(data));
|
|
115
|
+
return new SecureMessagingSession(p, keys, ssc);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Read the chip.
|
|
120
|
+
*
|
|
121
|
+
* DG1 is required — without the MRZ there is nothing to verify. EF.SOD and DG2
|
|
122
|
+
* are BEST-EFFORT and in that order: the SOD is what makes DG1 trustworthy, and
|
|
123
|
+
* DG2 is only worth reading if the SOD arrived, since an unverifiable portrait
|
|
124
|
+
* is one an attacker could have substituted.
|
|
125
|
+
*/
|
|
126
|
+
export async function readChip(
|
|
127
|
+
native: Parameters<typeof primitivesFromNative>[0] & EmrtdTransport,
|
|
128
|
+
mrz: MrzKeyFields,
|
|
129
|
+
/**
|
|
130
|
+
* Progress, so the UI can narrate a read that is otherwise invisible. Each
|
|
131
|
+
* call marks the step ABOUT to run, not the one just finished — the user
|
|
132
|
+
* needs to know what is happening now, and EF.SOD in particular takes long
|
|
133
|
+
* enough that silence reads as failure and prompts them to lift the document.
|
|
134
|
+
*/
|
|
135
|
+
onStage?: (stage: NfcReadStage) => void,
|
|
136
|
+
): Promise<EmrtdReadResult> {
|
|
137
|
+
const p = primitivesFromNative(native);
|
|
138
|
+
|
|
139
|
+
onStage?.('authenticating');
|
|
140
|
+
// SELECT + BAC are retried ONCE in place, on the same live connection.
|
|
141
|
+
// On Android the tag is routinely dispatched while the platform is still
|
|
142
|
+
// settling the link, so the first exchange dies with the chip right there —
|
|
143
|
+
// and an immediate second attempt succeeds with the phone untouched. The
|
|
144
|
+
// Flutter SDK documents and fixes this exact failure the same way
|
|
145
|
+
// (nfc_reader_emrtd.dart); a full session teardown cannot fix it, because
|
|
146
|
+
// Android never re-dispatches a tag that stayed in the field, so the outer
|
|
147
|
+
// retry loop just waited for a re-tap nobody knew to perform.
|
|
148
|
+
let sm: Awaited<ReturnType<typeof openSession>> | null = null;
|
|
149
|
+
for (let attempt = 0; sm === null; attempt += 1) {
|
|
150
|
+
try {
|
|
151
|
+
await selectApplication(native);
|
|
152
|
+
sm = await openSession(p, native, mrz);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
if (attempt >= 1) throw err;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const transceive: Transceive = async (command) => {
|
|
159
|
+
const { data, statusWord } = await native.transceive(toBase64(command));
|
|
160
|
+
// The wrapped response is what secure messaging verifies; the outer status
|
|
161
|
+
// word is not covered by the MAC and is not the one that counts.
|
|
162
|
+
const body = fromBase64(data);
|
|
163
|
+
const out = new Uint8Array(body.length + 2);
|
|
164
|
+
out.set(body);
|
|
165
|
+
out[body.length] = (statusWord >> 8) & 0xff;
|
|
166
|
+
out[body.length + 1] = statusWord & 0xff;
|
|
167
|
+
return out;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
onStage?.('readingData');
|
|
171
|
+
let dg1: Uint8Array;
|
|
172
|
+
try {
|
|
173
|
+
dg1 = await readFile(sm, transceive, EF.DG1);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
throw new EmrtdSessionError(
|
|
176
|
+
err instanceof Error ? err.message : 'The chip stopped responding.',
|
|
177
|
+
'read_failed',
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// From here on, a failure costs a signal rather than the read. A chip that
|
|
182
|
+
// gave up its MRZ has already proven it holds the BAC key. Optional reads
|
|
183
|
+
// get a second in-session try and NAME their failure — see optionalRead.ts.
|
|
184
|
+
onStage?.('readingSecurity');
|
|
185
|
+
const sod = await readOptionalFile(sm, transceive, EF.SOD, 'EF.SOD');
|
|
186
|
+
|
|
187
|
+
let dg2: Uint8Array | null = null;
|
|
188
|
+
if (sod) {
|
|
189
|
+
onStage?.('readingPhoto');
|
|
190
|
+
dg2 = await readOptionalFile(sm, transceive, EF.DG2, 'DG2');
|
|
191
|
+
}
|
|
192
|
+
onStage?.('done');
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
dg1: toBase64(dg1),
|
|
196
|
+
...(sod ? { sod: toBase64(sod) } : {}),
|
|
197
|
+
...(dg2 ? { dg2: toBase64(dg2) } : {}),
|
|
198
|
+
chipAuth: 'bac',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// How far a chip read has got.
|
|
3
|
+
//
|
|
4
|
+
// iOS puts a system sheet over the whole read and narrates it. Android has NO
|
|
5
|
+
// system NFC UI at all, so the same read is otherwise a bare spinner: the user
|
|
6
|
+
// cannot tell whether the chip has even been detected, let alone that three
|
|
7
|
+
// data groups are being pulled off it — and the natural response to that
|
|
8
|
+
// silence is to lift the document, which aborts the read.
|
|
9
|
+
//
|
|
10
|
+
// The wording is deliberately identical to the iOS sheet's, so both platforms
|
|
11
|
+
// narrate the read the same way. Ported from the Flutter SDK's NfcReadStage —
|
|
12
|
+
// keep the two in step.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export type NfcReadStage =
|
|
16
|
+
/** Session open, waiting for the user to actually present the chip. */
|
|
17
|
+
| 'waiting'
|
|
18
|
+
/** Chip detected; opening the BAC session with the MRZ key. */
|
|
19
|
+
| 'authenticating'
|
|
20
|
+
/** Reading DG1 — the document's printed details. */
|
|
21
|
+
| 'readingData'
|
|
22
|
+
/** Reading EF.SOD — kilobytes, so the long one, and the step users abandon. */
|
|
23
|
+
| 'readingSecurity'
|
|
24
|
+
/** Reading DG2 — the chip's portrait. */
|
|
25
|
+
| 'readingPhoto'
|
|
26
|
+
/** Everything that could be read has been. */
|
|
27
|
+
| 'done';
|
|
28
|
+
|
|
29
|
+
/** Order of the read, used to decide which steps are already complete. */
|
|
30
|
+
export const NFC_STAGE_ORDER: NfcReadStage[] = [
|
|
31
|
+
'waiting',
|
|
32
|
+
'authenticating',
|
|
33
|
+
'readingData',
|
|
34
|
+
'readingSecurity',
|
|
35
|
+
'readingPhoto',
|
|
36
|
+
'done',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** Short label for the step list. */
|
|
40
|
+
export function nfcStageLabel(stage: NfcReadStage): string {
|
|
41
|
+
switch (stage) {
|
|
42
|
+
case 'waiting':
|
|
43
|
+
return 'Waiting for the chip';
|
|
44
|
+
case 'authenticating':
|
|
45
|
+
return 'Unlocking the chip';
|
|
46
|
+
case 'readingData':
|
|
47
|
+
return 'Reading document details';
|
|
48
|
+
case 'readingSecurity':
|
|
49
|
+
return 'Reading security data';
|
|
50
|
+
case 'readingPhoto':
|
|
51
|
+
return 'Reading photo';
|
|
52
|
+
case 'done':
|
|
53
|
+
return 'Chip read complete';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* One line on what the phone is actually doing.
|
|
59
|
+
*
|
|
60
|
+
* A chip read is invisible — no shutter, no preview — so the detail is what
|
|
61
|
+
* stops a pause reading as a failure and prompting the user to lift the
|
|
62
|
+
* document.
|
|
63
|
+
*/
|
|
64
|
+
export function nfcStageDetail(stage: NfcReadStage): string {
|
|
65
|
+
switch (stage) {
|
|
66
|
+
case 'waiting':
|
|
67
|
+
return 'Hold the top edge of your document flat against the back of your phone.';
|
|
68
|
+
case 'authenticating':
|
|
69
|
+
return 'Opening a secure session using the code printed on your photo page.';
|
|
70
|
+
case 'readingData':
|
|
71
|
+
return 'Copying the details stored on the chip.';
|
|
72
|
+
case 'readingSecurity':
|
|
73
|
+
return 'Downloading the chip’s digital signature. This is the largest part and takes the longest — keep holding.';
|
|
74
|
+
case 'readingPhoto':
|
|
75
|
+
return 'Copying the photo stored on the chip.';
|
|
76
|
+
case 'done':
|
|
77
|
+
return 'Everything was read successfully.';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The line shown on iOS's system NFC sheet while a stage runs.
|
|
83
|
+
*
|
|
84
|
+
* The sheet COVERS the app during the read, so this is the only narration an
|
|
85
|
+
* iOS user gets. Terser than the in-app detail — the sheet truncates — and the
|
|
86
|
+
* long transfers say "keep holding", because lifting during them is exactly
|
|
87
|
+
* how the security file and the photo get lost.
|
|
88
|
+
*/
|
|
89
|
+
export function nfcSheetMessage(stage: NfcReadStage): string {
|
|
90
|
+
switch (stage) {
|
|
91
|
+
case 'waiting':
|
|
92
|
+
return 'Hold your phone near the passport';
|
|
93
|
+
case 'authenticating':
|
|
94
|
+
return 'Chip found — unlocking it…';
|
|
95
|
+
case 'readingData':
|
|
96
|
+
return 'Reading document details…';
|
|
97
|
+
case 'readingSecurity':
|
|
98
|
+
return 'Verifying security data — keep holding…';
|
|
99
|
+
case 'readingPhoto':
|
|
100
|
+
return 'Reading your photo — keep holding…';
|
|
101
|
+
case 'done':
|
|
102
|
+
return 'Chip read complete';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Position in the read, 0..1 — drives the progress bar. */
|
|
107
|
+
export function nfcStageProgress(stage: NfcReadStage): number {
|
|
108
|
+
switch (stage) {
|
|
109
|
+
case 'waiting':
|
|
110
|
+
return 0;
|
|
111
|
+
case 'authenticating':
|
|
112
|
+
return 0.15;
|
|
113
|
+
case 'readingData':
|
|
114
|
+
return 0.35;
|
|
115
|
+
case 'readingSecurity':
|
|
116
|
+
return 0.6;
|
|
117
|
+
case 'readingPhoto':
|
|
118
|
+
return 0.85;
|
|
119
|
+
case 'done':
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type { MyazaKYCProps, UseMyazaKYCReturn } from './MyazaKYC';
|
|
|
14
14
|
// Public config + callback types
|
|
15
15
|
export type {
|
|
16
16
|
MyazaKYCConfig,
|
|
17
|
+
ResolvedKYCConfig,
|
|
17
18
|
SupportedCountry,
|
|
18
19
|
IdType,
|
|
19
20
|
IdTypeForCountry,
|
|
@@ -24,7 +25,36 @@ export type {
|
|
|
24
25
|
KYCSuccessContent,
|
|
25
26
|
VoiceGuidanceConfig,
|
|
26
27
|
VoiceGuidanceOption,
|
|
28
|
+
// Workflow-driven blocks — normally authored in the dashboard builder and
|
|
29
|
+
// delivered by `workflowId`, but settable as props too.
|
|
30
|
+
EmailVerificationConfig,
|
|
31
|
+
PhoneVerificationConfig,
|
|
32
|
+
ProofOfAddressConfig,
|
|
33
|
+
PoaDocumentType,
|
|
34
|
+
QuestionnaireConfig,
|
|
35
|
+
QuestionnaireField,
|
|
36
|
+
QuestionnaireFieldOption,
|
|
37
|
+
QuestionnaireAnswerValue,
|
|
38
|
+
OtpInputStyle,
|
|
39
|
+
NfcConfig,
|
|
40
|
+
LivenessMode,
|
|
41
|
+
WorkflowCountry,
|
|
42
|
+
WorkflowIdOption,
|
|
27
43
|
} from './types/config';
|
|
44
|
+
export type {
|
|
45
|
+
SubjectType,
|
|
46
|
+
ApplicantRole,
|
|
47
|
+
KeyPersonRole,
|
|
48
|
+
KeyPeopleLevel,
|
|
49
|
+
BusinessDocumentKey,
|
|
50
|
+
CompanyInfoField,
|
|
51
|
+
CompanyInfoMode,
|
|
52
|
+
WorkflowBusinessConfig,
|
|
53
|
+
WorkflowKeyPeopleConfig,
|
|
54
|
+
WorkflowBusinessDocumentsConfig,
|
|
55
|
+
WorkflowBusinessDocumentTypeConfig,
|
|
56
|
+
WorkflowBusinessApplicantConfig,
|
|
57
|
+
} from './types/business';
|
|
28
58
|
export type { KYCSubmission, KYCErrorCode, KYCErrorDetails } from './types/verification';
|
|
29
59
|
export { KYCError } from './types/verification';
|
|
30
60
|
|
|
@@ -35,8 +65,51 @@ export {
|
|
|
35
65
|
isNumberOnlyIdType,
|
|
36
66
|
requiresDocumentCapture,
|
|
37
67
|
getScanSides,
|
|
68
|
+
supportsNfcChip,
|
|
38
69
|
} from './config/idTypes';
|
|
39
70
|
|
|
71
|
+
// Flow shape — the single ordered list navigation, the back button and the
|
|
72
|
+
// progress indicator all read.
|
|
73
|
+
export {
|
|
74
|
+
buildStepOrder,
|
|
75
|
+
getStepProgress,
|
|
76
|
+
nextStepInOrder,
|
|
77
|
+
previousStepInOrder,
|
|
78
|
+
type StepOrderOptions,
|
|
79
|
+
} from './config/stepOrder';
|
|
80
|
+
|
|
81
|
+
// Business (KYB) product catalogue + application gates
|
|
82
|
+
export {
|
|
83
|
+
BUSINESS_PRODUCTS,
|
|
84
|
+
DEFAULT_BUSINESS_PRODUCT,
|
|
85
|
+
businessCountriesFor,
|
|
86
|
+
businessProductsFor,
|
|
87
|
+
businessProductsForCountry,
|
|
88
|
+
companyInfoFieldModes,
|
|
89
|
+
getBusinessProductDef,
|
|
90
|
+
isBusinessFlow,
|
|
91
|
+
type BusinessProductDef,
|
|
92
|
+
} from './config/business';
|
|
93
|
+
export {
|
|
94
|
+
businessSectionSteps,
|
|
95
|
+
hasApplicantVerification,
|
|
96
|
+
hasBusinessDocumentsStep,
|
|
97
|
+
hasKeyPeopleCollection,
|
|
98
|
+
keyPeopleMinEntries,
|
|
99
|
+
resolveBusinessDocumentTypes,
|
|
100
|
+
type BusinessSectionStep,
|
|
101
|
+
type ResolvedBusinessDocumentType,
|
|
102
|
+
} from './config/businessSteps';
|
|
103
|
+
|
|
104
|
+
// Optional-step gates — the same helpers the step order reads, exported so a
|
|
105
|
+
// consumer can reason about which steps a given config will run.
|
|
106
|
+
export { hasActiveQuestionnaire, questionnaireAnswerKeys } from './config/questionnaire';
|
|
107
|
+
export { hasEmailVerificationStep, hasPhoneVerificationStep } from './config/contact';
|
|
108
|
+
export { hasProofOfAddressStep, poaDocumentTypes, poaTypeLabel } from './config/proofOfAddress';
|
|
109
|
+
|
|
110
|
+
// Country grouping for multi-region flows
|
|
111
|
+
export { groupCountriesByRegion, regionCountryName, type Region } from './config/regions';
|
|
112
|
+
|
|
40
113
|
// Client-side validation
|
|
41
114
|
export { validateIdNumber, maskIdNumber, type ValidationResult } from './services/validators';
|
|
42
115
|
|
|
@@ -50,5 +123,42 @@ export {
|
|
|
50
123
|
hasFaceDetectorFactory,
|
|
51
124
|
} from './liveness/faceDetector';
|
|
52
125
|
|
|
126
|
+
// Device Intelligence — the fingerprint the server scores. Exported so a
|
|
127
|
+
// consumer can see exactly what is collected.
|
|
128
|
+
export {
|
|
129
|
+
collectFingerprint,
|
|
130
|
+
type ClientFingerprint,
|
|
131
|
+
type FingerprintComponents,
|
|
132
|
+
} from './services/fingerprint';
|
|
133
|
+
|
|
134
|
+
// MRZ reading (ICAO 9303) — exported so a consumer can parse a document's
|
|
135
|
+
// machine-readable zone with the same check-digit validation the SDK uses.
|
|
136
|
+
export { mrzCheckDigit, parseMrz, type MrzScan } from './mrz/parse';
|
|
137
|
+
// Capability probes, so a consumer can tell "this build cannot read text" from
|
|
138
|
+
// "this document has no MRZ" — the two look identical from the outside, and
|
|
139
|
+
// only the first is worth reporting as a build problem.
|
|
140
|
+
export { hasTextRecognizer } from './mrz/textRecognizer';
|
|
141
|
+
export { hasRectDetector } from './capture/rectDetector';
|
|
142
|
+
export { extractMrz, sanitizeMrzLine } from './mrz/extract';
|
|
143
|
+
|
|
144
|
+
// eMRTD chip reading. The SERVER decides authenticity — it hashes DG1 against
|
|
145
|
+
// the signed security object and verifies the document signer. Nothing here
|
|
146
|
+
// concludes that a chip is genuine.
|
|
147
|
+
export {
|
|
148
|
+
isNfcAvailable,
|
|
149
|
+
readPassportChip,
|
|
150
|
+
EmrtdSessionError,
|
|
151
|
+
type EmrtdReadResult,
|
|
152
|
+
} from './emrtd';
|
|
153
|
+
|
|
154
|
+
// Flash (screen-reflection) liveness — the palette and correlation are a
|
|
155
|
+
// contract with the server's own re-analysis of the recorded video.
|
|
156
|
+
export {
|
|
157
|
+
FLASH_PALETTE,
|
|
158
|
+
generateFlashSequence,
|
|
159
|
+
type FlashColor,
|
|
160
|
+
type FlashResult,
|
|
161
|
+
} from './liveness/flashDetector';
|
|
162
|
+
|
|
53
163
|
// SDK version
|
|
54
164
|
export { SDK_VERSION } from './services/deviceMetadata';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE month-grid math for the date picker — no RN imports, so it is unit
|
|
3
|
+
* testable. Dates are handled as plain {year, month0, day} numbers and ISO
|
|
4
|
+
* `YYYY-MM-DD` strings; never Date objects at the edges, because
|
|
5
|
+
* `new Date('YYYY-MM-DD')` parses as UTC midnight and shifts a birthday by a
|
|
6
|
+
* day for every user west of Greenwich.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const MONTH_NAMES = [
|
|
10
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
11
|
+
'July', 'August', 'September', 'October', 'November', 'December',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export const WEEKDAY_INITIALS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'] as const;
|
|
15
|
+
|
|
16
|
+
export interface CalendarDate {
|
|
17
|
+
year: number;
|
|
18
|
+
/** 0-based, matching Date#getMonth. */
|
|
19
|
+
month0: number;
|
|
20
|
+
day: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function daysInMonth(year: number, month0: number): number {
|
|
24
|
+
return new Date(year, month0 + 1, 0).getDate();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The weeks of one month, Sunday-first, padded with nulls so every row is
|
|
29
|
+
* seven cells — the shape a calendar grid renders directly.
|
|
30
|
+
*/
|
|
31
|
+
export function monthMatrix(year: number, month0: number): (number | null)[][] {
|
|
32
|
+
const firstWeekday = new Date(year, month0, 1).getDay();
|
|
33
|
+
const total = daysInMonth(year, month0);
|
|
34
|
+
|
|
35
|
+
const cells: (number | null)[] = [
|
|
36
|
+
...Array.from({ length: firstWeekday }, () => null),
|
|
37
|
+
...Array.from({ length: total }, (_, i) => i + 1),
|
|
38
|
+
];
|
|
39
|
+
while (cells.length % 7 !== 0) cells.push(null);
|
|
40
|
+
|
|
41
|
+
const weeks: (number | null)[][] = [];
|
|
42
|
+
for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
|
|
43
|
+
return weeks;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function toIsoDate({ year, month0, day }: CalendarDate): string {
|
|
47
|
+
const mm = String(month0 + 1).padStart(2, '0');
|
|
48
|
+
const dd = String(day).padStart(2, '0');
|
|
49
|
+
return `${year}-${mm}-${dd}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Strictly `YYYY-MM-DD` and a real calendar day — anything else is null. */
|
|
53
|
+
export function parseIsoDate(value: string | undefined | null): CalendarDate | null {
|
|
54
|
+
if (!value) return null;
|
|
55
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
|
|
56
|
+
if (!m) return null;
|
|
57
|
+
const year = Number(m[1]);
|
|
58
|
+
const month0 = Number(m[2]) - 1;
|
|
59
|
+
const day = Number(m[3]);
|
|
60
|
+
if (month0 < 0 || month0 > 11) return null;
|
|
61
|
+
if (day < 1 || day > daysInMonth(year, month0)) return null;
|
|
62
|
+
return { year, month0, day };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Step a (year, month0) cursor by ±1 month, carrying across year edges. */
|
|
66
|
+
export function addMonth(year: number, month0: number, delta: 1 | -1): { year: number; month0: number } {
|
|
67
|
+
const next = month0 + delta;
|
|
68
|
+
if (next < 0) return { year: year - 1, month0: 11 };
|
|
69
|
+
if (next > 11) return { year: year + 1, month0: 0 };
|
|
70
|
+
return { year, month0: next };
|
|
71
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Keyboard, Platform } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The height the software keyboard currently covers, as a bottom inset.
|
|
6
|
+
*
|
|
7
|
+
* Exists for ANDROID. The flow presents inside a `statusBarTranslucent`
|
|
8
|
+
* `<Modal>`, and on Android that combination disables `adjustResize` — the
|
|
9
|
+
* window never shrinks for the keyboard, so a ScrollView inside it never
|
|
10
|
+
* learns the bottom half of the screen is gone and the focused input stays
|
|
11
|
+
* buried. (iOS doesn't use this: the ScrollView's
|
|
12
|
+
* `automaticallyAdjustKeyboardInsets` is the platform's own, better handling —
|
|
13
|
+
* it insets AND reveals the focused input.)
|
|
14
|
+
*
|
|
15
|
+
* Padding the scroll content by this amount restores what adjustResize would
|
|
16
|
+
* have done: everything above the keyboard is reachable by scrolling.
|
|
17
|
+
*/
|
|
18
|
+
export function useKeyboardInset(): number {
|
|
19
|
+
const [inset, setInset] = useState(0);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (Platform.OS !== 'android') return undefined;
|
|
23
|
+
const show = Keyboard.addListener('keyboardDidShow', (e) =>
|
|
24
|
+
setInset(e.endCoordinates.height),
|
|
25
|
+
);
|
|
26
|
+
const hide = Keyboard.addListener('keyboardDidHide', () => setInset(0));
|
|
27
|
+
return () => {
|
|
28
|
+
show.remove();
|
|
29
|
+
hide.remove();
|
|
30
|
+
};
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
return inset;
|
|
34
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ChallengeConfig,
|
|
11
11
|
type LivenessChallenge,
|
|
12
12
|
type LivenessConfig,
|
|
13
|
+
type LivenessMode,
|
|
13
14
|
} from './types';
|
|
14
15
|
|
|
15
16
|
// Gestures within a similarity group should never appear together — one can
|
|
@@ -29,9 +30,34 @@ function shuffle<T>(arr: readonly T[]): T[] {
|
|
|
29
30
|
return copy;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Whether the workflow's liveness mode runs gesture challenges at all.
|
|
35
|
+
*
|
|
36
|
+
* `flash` means the screen-reflection sequence IS the check, so gestures are
|
|
37
|
+
* skipped entirely — only `both` runs gestures and then flash. Kept in step
|
|
38
|
+
* with the Flutter SDK's `runsGestures` and the web SDK.
|
|
39
|
+
*/
|
|
40
|
+
export function modeRunsGestures(mode: LivenessMode | undefined): boolean {
|
|
41
|
+
return mode !== 'flash';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Picks `challengeCount` non-similar challenges from the pool, in random order.
|
|
46
|
+
*
|
|
47
|
+
* Returns NOTHING in flash-only mode. This is gated here, at the single place
|
|
48
|
+
* challenges are chosen, rather than at the call sites: `useLiveness` rebuilds
|
|
49
|
+
* the tracker in three separate places (mount, retry, face-change reset), and a
|
|
50
|
+
* check in one of them would leave the others silently running gestures a
|
|
51
|
+
* flash-only workflow explicitly turned off.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately keyed on the mode rather than `challengeCount: 0` — the count is
|
|
54
|
+
* clamped to at least one on purpose, so that a stray zero in a consumer's
|
|
55
|
+
* config can never quietly disable gesture liveness. Skipping gestures has to
|
|
56
|
+
* be asked for.
|
|
57
|
+
*/
|
|
33
58
|
export function pickChallenges(config: Partial<LivenessConfig> = {}): ChallengeConfig[] {
|
|
34
59
|
const merged = { ...DEFAULT_LIVENESS_CONFIG, ...config };
|
|
60
|
+
if (!modeRunsGestures(merged.mode)) return [];
|
|
35
61
|
|
|
36
62
|
let pool = CHALLENGE_POOL;
|
|
37
63
|
if (merged.challengePool && merged.challengePool.length > 0) {
|