@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
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { KYCApiError } from './api';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// User-facing copy for the contact-verification send/check failures.
|
|
5
|
+
//
|
|
6
|
+
// Each message says what happened AND what to do next, because every one of
|
|
7
|
+
// these is recoverable and the user is mid-flow. A bare "something went wrong"
|
|
8
|
+
// on an expired code leaves them retyping the same dead digits.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
const CHECK_ERRORS: Record<string, string> = {
|
|
12
|
+
invalid_code: 'That code is not correct. Please try again.',
|
|
13
|
+
challenge_expired: 'This code has expired. Request a new one.',
|
|
14
|
+
too_many_attempts: 'Too many incorrect attempts. Request a new code.',
|
|
15
|
+
challenge_not_found: 'This code is no longer valid. Request a new one.',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const SEND_ERRORS: Record<string, string> = {
|
|
19
|
+
invalid_destination: 'That does not look valid. Please check and try again.',
|
|
20
|
+
send_rate_limited: 'Too many codes requested. Please wait a while and try again.',
|
|
21
|
+
send_failed: 'We could not send the code. Please try again.',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function describe(err: unknown, map: Record<string, string>): string {
|
|
25
|
+
if (err instanceof KYCApiError && err.code && map[err.code]) return map[err.code]!;
|
|
26
|
+
if (err instanceof TypeError) return 'Network error. Check your connection and try again.';
|
|
27
|
+
return 'Something went wrong. Please try again.';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const describeSendError = (err: unknown): string => describe(err, SEND_ERRORS);
|
|
31
|
+
export const describeCheckError = (err: unknown): string => describe(err, CHECK_ERRORS);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Attempts left after a wrong code, when the server reports it.
|
|
35
|
+
*
|
|
36
|
+
* Showing the count is what stops a user burning the whole budget guessing —
|
|
37
|
+
* absent it they have no signal that the code is about to die.
|
|
38
|
+
*/
|
|
39
|
+
export function attemptsRemaining(err: unknown): number | undefined {
|
|
40
|
+
if (!(err instanceof KYCApiError)) return undefined;
|
|
41
|
+
const value = err.body?.attemptsRemaining;
|
|
42
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
43
|
+
}
|
|
@@ -14,7 +14,7 @@ export type DeviceType = 'mobile' | 'tablet' | 'desktop' | 'unknown';
|
|
|
14
14
|
* Single source of truth for the SDK version — also used by `services/api.ts`
|
|
15
15
|
* for the `X-SDK-Version` header. Keep in sync with `package.json`.
|
|
16
16
|
*/
|
|
17
|
-
export const SDK_VERSION = '2.0
|
|
17
|
+
export const SDK_VERSION = '2.1.0';
|
|
18
18
|
|
|
19
19
|
export interface ReactNativeDeviceMetadata {
|
|
20
20
|
sdkType: 'react-native';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `expo-document-picker`, loaded on demand.
|
|
3
|
+
*
|
|
4
|
+
* A static import runs the module's native lookup at MODULE load, which throws
|
|
5
|
+
* `Cannot find native module 'ExpoDocumentPicker'` on any host app that has not
|
|
6
|
+
* rebuilt to pick it up — taking down the whole SDK, including flows that never
|
|
7
|
+
* choose a file. Requiring it at the moment the user taps means the capability
|
|
8
|
+
* degrades to photo-only where it is unavailable, matching the null-degrade
|
|
9
|
+
* posture the platform uses for every other optional native dependency.
|
|
10
|
+
*/
|
|
11
|
+
export type DocumentPickerModule = typeof import('expo-document-picker');
|
|
12
|
+
|
|
13
|
+
export function loadDocumentPicker(): DocumentPickerModule | null {
|
|
14
|
+
try {
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
|
+
return require('expo-document-picker') as DocumentPickerModule;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { OS } from '../utils/platform';
|
|
2
|
+
|
|
3
|
+
import type { FingerprintComponents } from './fingerprint';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Where each fingerprint component comes from.
|
|
7
|
+
//
|
|
8
|
+
// Split from fingerprint.ts (200-line rule). Every accessor is guarded: a host
|
|
9
|
+
// app missing one of these Expo modules must lose that ONE signal, not the
|
|
10
|
+
// whole fingerprint and certainly not the submission.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Runs a literal `require(...)`, returning undefined if it throws.
|
|
15
|
+
*
|
|
16
|
+
* Metro needs a STRING LITERAL module name, so each caller passes its own
|
|
17
|
+
* loader. Under the Node test runner the native modules do not resolve and the
|
|
18
|
+
* loader throws — caught here, which is what lets these tests run at all.
|
|
19
|
+
*/
|
|
20
|
+
export function tryRequire<T>(loader: () => T): T | undefined {
|
|
21
|
+
try {
|
|
22
|
+
return loader();
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ExpoDeviceModule {
|
|
29
|
+
brand?: string | null;
|
|
30
|
+
manufacturer?: string | null;
|
|
31
|
+
modelName?: string | null;
|
|
32
|
+
isDevice?: boolean;
|
|
33
|
+
osVersion?: string | null;
|
|
34
|
+
osBuildId?: string | null;
|
|
35
|
+
totalMemory?: number | null;
|
|
36
|
+
supportedCpuArchitectures?: string[] | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ExpoApplicationModule {
|
|
40
|
+
getIosIdForVendorAsync?: () => Promise<string | null>;
|
|
41
|
+
getAndroidId?: () => string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ExpoLocalizationModule {
|
|
45
|
+
getLocales?: () => Array<{ languageTag?: string }>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* React Native's own APIs, behind the same guarded require the rest of the SDK's
|
|
50
|
+
* pure modules use (see utils/platform.ts).
|
|
51
|
+
*
|
|
52
|
+
* A static `import … from 'react-native'` here would pull the whole RN module
|
|
53
|
+
* graph into the STORE's import chain, which the plain-Node test runner cannot
|
|
54
|
+
* parse — taking the flow-navigation tests down with it. This is the only
|
|
55
|
+
* reason the fingerprint reaches RN indirectly.
|
|
56
|
+
*/
|
|
57
|
+
interface ReactNativeApis {
|
|
58
|
+
Dimensions?: { get: (dim: string) => { width: number; height: number } };
|
|
59
|
+
PixelRatio?: { get: () => number; getFontScale: () => number };
|
|
60
|
+
Platform?: { Version?: string | number };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function reactNative(): ReactNativeApis | undefined {
|
|
64
|
+
return tryRequire<ReactNativeApis>(() => require('react-native'));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `Platform.Version` is a string on iOS and a number (API level) on Android. */
|
|
68
|
+
export function platformVersion(): string | undefined {
|
|
69
|
+
const version = reactNative()?.Platform?.Version;
|
|
70
|
+
if (version === undefined || version === null) return undefined;
|
|
71
|
+
const text = String(version);
|
|
72
|
+
return text === '' ? undefined : text;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function safeTimezone(): string | undefined {
|
|
76
|
+
try {
|
|
77
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
78
|
+
} catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function safeLanguages(): string[] | undefined {
|
|
84
|
+
const localization = tryRequire<ExpoLocalizationModule>(() => require('expo-localization'));
|
|
85
|
+
try {
|
|
86
|
+
const tags = localization?.getLocales?.()
|
|
87
|
+
.map((l) => l.languageTag)
|
|
88
|
+
.filter((tag): tag is string => typeof tag === 'string' && tag !== '');
|
|
89
|
+
return tags && tags.length > 0 ? tags : undefined;
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function safeScreen(): FingerprintComponents['screen'] {
|
|
96
|
+
try {
|
|
97
|
+
const rn = reactNative();
|
|
98
|
+
if (!rn?.Dimensions || !rn.PixelRatio) return undefined;
|
|
99
|
+
const { width, height } = rn.Dimensions.get('screen');
|
|
100
|
+
// Rounded because RN reports fractional logical pixels on some devices, and
|
|
101
|
+
// a hash that changes with a rounding wobble is not an identifier.
|
|
102
|
+
return {
|
|
103
|
+
width: Math.round(width),
|
|
104
|
+
height: Math.round(height),
|
|
105
|
+
scale: rn.PixelRatio.get(),
|
|
106
|
+
fontScale: rn.PixelRatio.getFontScale(),
|
|
107
|
+
};
|
|
108
|
+
} catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The OS-vended per-install id. Async because iOS's is. */
|
|
114
|
+
export async function persistentDeviceId(): Promise<string | undefined> {
|
|
115
|
+
const application = tryRequire<ExpoApplicationModule>(() => require('expo-application'));
|
|
116
|
+
if (!application) return undefined;
|
|
117
|
+
try {
|
|
118
|
+
if (OS === 'ios') return (await application.getIosIdForVendorAsync?.()) ?? undefined;
|
|
119
|
+
if (OS === 'android') return application.getAndroidId?.() ?? undefined;
|
|
120
|
+
} catch {
|
|
121
|
+
/* the OS declined — the fingerprint still works without it */
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** expo-device, or undefined when it is not installed. */
|
|
127
|
+
export function expoDevice(): ExpoDeviceModule | undefined {
|
|
128
|
+
return tryRequire<ExpoDeviceModule>(() => require('expo-device'));
|
|
129
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { OS } from '../utils/platform';
|
|
2
|
+
import {
|
|
3
|
+
persistentDeviceId,
|
|
4
|
+
safeLanguages,
|
|
5
|
+
safeScreen,
|
|
6
|
+
safeTimezone,
|
|
7
|
+
platformVersion,
|
|
8
|
+
expoDevice,
|
|
9
|
+
} from './fingerprint-sources';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Device Intelligence — the client fingerprint.
|
|
13
|
+
//
|
|
14
|
+
// The SDK sends RAW components and never a self-computed id: the server
|
|
15
|
+
// canonicalises and hashes them, so a client cannot mint a fresh identity by
|
|
16
|
+
// posting a random hash. Everything is best-effort — a component that cannot be
|
|
17
|
+
// read is simply omitted, and the server scores what it gets.
|
|
18
|
+
//
|
|
19
|
+
// The components differ from the web SDK's on purpose. There is no WebGL
|
|
20
|
+
// renderer or canvas to hash here, but a native app can answer questions a
|
|
21
|
+
// browser cannot: whether it is running on real hardware rather than an
|
|
22
|
+
// emulator, and a per-install identifier the OS itself vends. Those are
|
|
23
|
+
// stronger signals than anything the browser fingerprint recovers.
|
|
24
|
+
//
|
|
25
|
+
// `deviceHash` is a RISK SIGNAL, never an identity link — phone models collide
|
|
26
|
+
// constantly, and merging two people because they own the same handset would
|
|
27
|
+
// corrupt the identity graph.
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export interface FingerprintComponents {
|
|
31
|
+
screen?: { width: number; height: number; scale: number; fontScale: number };
|
|
32
|
+
timezone?: string;
|
|
33
|
+
languages?: string[];
|
|
34
|
+
platform?: string;
|
|
35
|
+
osVersion?: string;
|
|
36
|
+
/** Device maker + model, as the OS reports them. */
|
|
37
|
+
brand?: string;
|
|
38
|
+
manufacturer?: string;
|
|
39
|
+
model?: string;
|
|
40
|
+
/** Total RAM in bytes — the native counterpart of the web's deviceMemory. */
|
|
41
|
+
totalMemory?: number;
|
|
42
|
+
cpuArchitectures?: string[];
|
|
43
|
+
/** OS build identifier — differs between a stock ROM and a modified one. */
|
|
44
|
+
osBuildId?: string;
|
|
45
|
+
/**
|
|
46
|
+
* FALSE means an emulator or simulator. The single most useful signal here:
|
|
47
|
+
* a real user is not verifying their identity from an Android emulator, and
|
|
48
|
+
* unlike the web's headless heuristics this is a direct answer from the OS.
|
|
49
|
+
*/
|
|
50
|
+
isPhysicalDevice?: boolean;
|
|
51
|
+
/** True when the app is running from a development client / debug build. */
|
|
52
|
+
isDevelopmentBuild?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ClientFingerprint {
|
|
56
|
+
/**
|
|
57
|
+
* Per-install identifier vended by the OS — `identifierForVendor` on iOS,
|
|
58
|
+
* `ANDROID_ID` on Android. Preferred over a self-generated UUID in storage:
|
|
59
|
+
* it survives app data being cleared on iOS, needs no storage dependency, and
|
|
60
|
+
* cannot be spoofed from JavaScript. Resets on uninstall, which is correct —
|
|
61
|
+
* it identifies an install, not a person.
|
|
62
|
+
*/
|
|
63
|
+
deviceId?: string;
|
|
64
|
+
components: FingerprintComponents;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Collect the fingerprint.
|
|
69
|
+
*
|
|
70
|
+
* Async only because iOS's vendor id is; everything else is synchronous. It is
|
|
71
|
+
* called once at submit and must never throw — a fingerprint that fails to
|
|
72
|
+
* collect is a missing signal, not a failed verification.
|
|
73
|
+
*/
|
|
74
|
+
export async function collectFingerprint(): Promise<ClientFingerprint> {
|
|
75
|
+
const device = expoDevice();
|
|
76
|
+
|
|
77
|
+
const components: FingerprintComponents = {
|
|
78
|
+
screen: safeScreen(),
|
|
79
|
+
timezone: safeTimezone(),
|
|
80
|
+
languages: safeLanguages(),
|
|
81
|
+
platform: OS,
|
|
82
|
+
osVersion: device?.osVersion ?? platformVersion(),
|
|
83
|
+
brand: device?.brand ?? undefined,
|
|
84
|
+
manufacturer: device?.manufacturer ?? undefined,
|
|
85
|
+
model: device?.modelName ?? undefined,
|
|
86
|
+
totalMemory: device?.totalMemory ?? undefined,
|
|
87
|
+
cpuArchitectures: device?.supportedCpuArchitectures ?? undefined,
|
|
88
|
+
osBuildId: device?.osBuildId ?? undefined,
|
|
89
|
+
isPhysicalDevice: device?.isDevice,
|
|
90
|
+
isDevelopmentBuild: typeof __DEV__ === 'boolean' ? __DEV__ : undefined,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
deviceId: await persistentDeviceId(),
|
|
95
|
+
// Undefined entries are stripped so the server canonicalises the same shape
|
|
96
|
+
// whether a module was missing or a value was genuinely absent.
|
|
97
|
+
components: Object.fromEntries(
|
|
98
|
+
Object.entries(components).filter(([, v]) => v !== undefined),
|
|
99
|
+
) as FingerprintComponents,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Vibration } from 'react-native';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A success buzz, best-effort.
|
|
5
|
+
*
|
|
6
|
+
* A chip read has no shutter, no sound and nothing physical — the haptic is
|
|
7
|
+
* what makes it land as a completed action rather than a screen that quietly
|
|
8
|
+
* changed.
|
|
9
|
+
*
|
|
10
|
+
* `expo-haptics` gives the proper taptic pattern, but it is NOT a dependency of
|
|
11
|
+
* this SDK: adding one for a single buzz would force every consumer to rebuild.
|
|
12
|
+
* So it is loaded on demand where the host app already has it, and otherwise
|
|
13
|
+
* this falls back to RN's built-in vibrate, which needs nothing. Hardware with
|
|
14
|
+
* no taptic engine does nothing at all, which is also fine.
|
|
15
|
+
*/
|
|
16
|
+
export function successHaptic(): void {
|
|
17
|
+
try {
|
|
18
|
+
// Typed structurally rather than via `typeof import(...)`: the package is
|
|
19
|
+
// not a dependency here, so a type import would fail to resolve and break
|
|
20
|
+
// the build for the sake of a buzz.
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
|
+
const haptics = require('expo-haptics') as {
|
|
23
|
+
notificationAsync: (t: unknown) => Promise<void>;
|
|
24
|
+
NotificationFeedbackType: { Success: unknown };
|
|
25
|
+
};
|
|
26
|
+
void haptics
|
|
27
|
+
.notificationAsync(haptics.NotificationFeedbackType.Success)
|
|
28
|
+
.catch(() => undefined);
|
|
29
|
+
return;
|
|
30
|
+
} catch {
|
|
31
|
+
// Not installed — fall through to the built-in.
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
Vibration.vibrate(30);
|
|
35
|
+
} catch {
|
|
36
|
+
// A device that refuses to vibrate is not a problem worth reporting.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -79,9 +79,14 @@ export async function cropImage(uri: string, rect: CropRect): Promise<string> {
|
|
|
79
79
|
* CameraViewfinder shows the preview `BoxFit.cover` in a fixed 3:4 box with the
|
|
80
80
|
* guide centred at 88% width and `aspect` height (mirrors the SVG overlay).
|
|
81
81
|
*/
|
|
82
|
-
export async function cropCardRegion(
|
|
82
|
+
export async function cropCardRegion(
|
|
83
|
+
uri: string,
|
|
84
|
+
aspect: number,
|
|
85
|
+
/** Aspect of the box the preview actually rendered in (width ÷ height). */
|
|
86
|
+
viewAr?: number,
|
|
87
|
+
): Promise<string> {
|
|
83
88
|
const { width, height } = await imageSize(uri);
|
|
84
|
-
return cropImage(uri, cardCropRect(width, height, aspect));
|
|
89
|
+
return cropImage(uri, cardCropRect(width, height, aspect, viewAr));
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
/** Compress a document still for OCR. Returns a new file URI. */
|
|
@@ -20,8 +20,8 @@ export type SdkEnvironment = 'development' | 'sandbox' | 'production';
|
|
|
20
20
|
/** Canonical base URLs for the non-development environments. */
|
|
21
21
|
const BASE_URLS: Record<Exclude<SdkEnvironment, 'development'>, string> = {
|
|
22
22
|
// Sandbox and production share the same host; the key prefix selects the env.
|
|
23
|
-
sandbox: 'https://
|
|
24
|
-
production: 'https://
|
|
23
|
+
sandbox: 'https://trust.myaza.app',
|
|
24
|
+
production: 'https://trust.myaza.app',
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createKYCApi } from './api';
|
|
2
|
+
import { KYCApiError } from './api';
|
|
3
|
+
import { resolveBaseUrl } from './resolveUrl';
|
|
4
|
+
import { mergeWorkflowConfig, overlayApplicantWorkflow } from '../config/workflowMerge';
|
|
5
|
+
import { KYCError } from '../types/verification';
|
|
6
|
+
import type { MyazaKYCConfig, ResolvedKYCConfig } from '../types/config';
|
|
7
|
+
import type { ServerConfigState } from '../store/serverConfig';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Resolve-before-mount.
|
|
11
|
+
//
|
|
12
|
+
// A config carrying a `workflowId` needs the published flow BEFORE anything
|
|
13
|
+
// renders: the flow supplies `country`, the step toggles and the appearance, so
|
|
14
|
+
// mounting first would show the wrong flow and then rearrange it under the user.
|
|
15
|
+
// One round trip also returns the org's ID-type allowlist and branding, so the
|
|
16
|
+
// separate `/config` call is skipped entirely.
|
|
17
|
+
//
|
|
18
|
+
// Two things the Flutter SDK's gate does NOT have and this one must:
|
|
19
|
+
//
|
|
20
|
+
// • a TIMEOUT — its barrier is `barrierDismissible: false` with no deadline,
|
|
21
|
+
// so a request that never settles (a captive-portal Wi-Fi, a dead VPN) hangs
|
|
22
|
+
// the user on a spinner with no way out;
|
|
23
|
+
// • CANCELLATION — closing the sheet mid-resolve leaves the request in flight
|
|
24
|
+
// and its completion handler pointed at a screen that is gone.
|
|
25
|
+
//
|
|
26
|
+
// Both live here rather than in the component, so a caller cannot forget them.
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/** How long to wait for a workflow before giving up. */
|
|
30
|
+
export const WORKFLOW_RESOLVE_TIMEOUT_MS = 15_000;
|
|
31
|
+
|
|
32
|
+
export interface WorkflowGateResult {
|
|
33
|
+
config: ResolvedKYCConfig;
|
|
34
|
+
/** Preloaded server config, so the flow skips its own `/config` fetch. */
|
|
35
|
+
serverConfig: ServerConfigState;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Distinguishes "we gave up waiting" from "the server said no". */
|
|
39
|
+
export class WorkflowTimeoutError extends Error {
|
|
40
|
+
constructor() {
|
|
41
|
+
super('Timed out loading the verification workflow.');
|
|
42
|
+
this.name = 'WorkflowTimeoutError';
|
|
43
|
+
Object.setPrototypeOf(this, WorkflowTimeoutError.prototype);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function toKycError(err: unknown): KYCError {
|
|
48
|
+
if (err instanceof WorkflowTimeoutError) {
|
|
49
|
+
return new KYCError(
|
|
50
|
+
'network_error',
|
|
51
|
+
'We could not reach the verification service. Check your connection and try again.',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (err instanceof KYCApiError) {
|
|
55
|
+
return new KYCError(
|
|
56
|
+
err.statusCode === 401 ? 'invalid_api_key' : 'invalid_workflow',
|
|
57
|
+
err.statusCode === 401
|
|
58
|
+
? 'Invalid API key. Please contact support.'
|
|
59
|
+
: 'This verification workflow could not be loaded. Please contact support.',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return new KYCError('invalid_workflow', 'This verification workflow could not be loaded.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the workflow and merge it over the props.
|
|
67
|
+
*
|
|
68
|
+
* `signal` aborts the request — pass the caller's so an abandoned resolve stops
|
|
69
|
+
* consuming the network rather than merely being ignored. Rejects with a
|
|
70
|
+
* {@link KYCError}; the timeout surfaces as `network_error` because that is what
|
|
71
|
+
* it is from the user's side.
|
|
72
|
+
*/
|
|
73
|
+
export async function resolveWorkflow(
|
|
74
|
+
config: MyazaKYCConfig,
|
|
75
|
+
signal?: AbortSignal,
|
|
76
|
+
): Promise<WorkflowGateResult> {
|
|
77
|
+
const api = createKYCApi(resolveBaseUrl(config.apiKey, config.devUrl), config.apiKey);
|
|
78
|
+
|
|
79
|
+
// The deadline is ours, not the platform's: React Native's fetch has no
|
|
80
|
+
// default request timeout, so without this a socket that is open but silent
|
|
81
|
+
// never settles.
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
const abort = (): void => controller.abort();
|
|
84
|
+
signal?.addEventListener('abort', abort);
|
|
85
|
+
let timedOut = false;
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
timedOut = true;
|
|
88
|
+
controller.abort();
|
|
89
|
+
}, WORKFLOW_RESOLVE_TIMEOUT_MS);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const res = await api.workflow(config.workflowId!, controller.signal);
|
|
93
|
+
return {
|
|
94
|
+
// KYB: the mapped applicant workflow's capture template overlays the
|
|
95
|
+
// merged config (and its id is recorded for stamping) — see
|
|
96
|
+
// overlayApplicantWorkflow.
|
|
97
|
+
config: overlayApplicantWorkflow(
|
|
98
|
+
res.applicantWorkflow,
|
|
99
|
+
mergeWorkflowConfig(res.config, config as unknown as Record<string, unknown>),
|
|
100
|
+
) as unknown as ResolvedKYCConfig,
|
|
101
|
+
serverConfig: {
|
|
102
|
+
status: 'ready',
|
|
103
|
+
idTypes: res.idTypes,
|
|
104
|
+
branding: res.branding,
|
|
105
|
+
environment: res.environment,
|
|
106
|
+
fatal: false,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
} catch (err) {
|
|
110
|
+
// An abort we caused by the deadline reads as a timeout; an abort the
|
|
111
|
+
// caller caused is a cancellation and must propagate untranslated, so the
|
|
112
|
+
// caller can tell "the user left" from "this failed".
|
|
113
|
+
if (timedOut) throw toKycError(new WorkflowTimeoutError());
|
|
114
|
+
if (signal?.aborted) throw err;
|
|
115
|
+
throw toKycError(err);
|
|
116
|
+
} finally {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
signal?.removeEventListener('abort', abort);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The config to mount with when there is no workflow to resolve.
|
|
124
|
+
*
|
|
125
|
+
* Without a `workflowId` the consumer must supply `country` themselves — the
|
|
126
|
+
* flow cannot pick ID types, guides or endpoints without one. Failing here with
|
|
127
|
+
* a named error beats the alternative, which is a flow that mounts and then
|
|
128
|
+
* shows an empty ID-type list.
|
|
129
|
+
*/
|
|
130
|
+
export function requireCountry(config: MyazaKYCConfig): ResolvedKYCConfig {
|
|
131
|
+
if (!config.country) {
|
|
132
|
+
throw new KYCError(
|
|
133
|
+
'invalid_config',
|
|
134
|
+
'A `country` is required unless you pass a `workflowId`.',
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return config as ResolvedKYCConfig;
|
|
138
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { HybridObject } from 'react-native-nitro-modules';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Nitro spec for eMRTD chip reading: the NFC transport, plus the crypto
|
|
5
|
+
// primitives the protocol is built from.
|
|
6
|
+
//
|
|
7
|
+
// WHY THE CRYPTO IS NATIVE. The protocol logic — APDU construction, TLV
|
|
8
|
+
// parsing, secure messaging, key derivation, passive authentication — lives in
|
|
9
|
+
// TypeScript, where it is testable against ICAO 9303's own worked examples
|
|
10
|
+
// without a passport in hand. But the PRIMITIVES (3DES, AES, SHA, CMAC) come
|
|
11
|
+
// from the platform: CommonCrypto on iOS, javax.crypto on Android. Both are
|
|
12
|
+
// audited, constant-time where it matters, and hardware-accelerated. A
|
|
13
|
+
// hand-written DES in JavaScript would be slower and, far worse, a novel
|
|
14
|
+
// implementation on a path that authenticates a passport.
|
|
15
|
+
//
|
|
16
|
+
// Everything crosses the bridge as base64, because Nitro has no byte-array
|
|
17
|
+
// type and hex would double the payload of a 256KB DG2.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/** How the chip was unlocked. */
|
|
21
|
+
export type EmrtdAccessMethod = 'bac' | 'pace' | 'none';
|
|
22
|
+
|
|
23
|
+
export interface EmrtdTagInfo {
|
|
24
|
+
/** True once a chip is in range and an ISO-DEP channel is open. */
|
|
25
|
+
connected: boolean;
|
|
26
|
+
/** Card UID, base64. Diagnostic only — never an identity signal. */
|
|
27
|
+
uid: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EmrtdApduResponse {
|
|
31
|
+
/** Response data (no status word), base64. */
|
|
32
|
+
data: string;
|
|
33
|
+
/** SW1<<8 | SW2. 0x9000 is success; 0x63CF and 0x6Cxx are meaningful too. */
|
|
34
|
+
statusWord: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MyazaEmrtd extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> {
|
|
38
|
+
/** Whether this device has NFC hardware capable of ISO-DEP. */
|
|
39
|
+
isAvailable(): boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Begin a session and wait for a chip.
|
|
43
|
+
*
|
|
44
|
+
* `alertMessage` is shown in iOS's system NFC sheet; Android has no
|
|
45
|
+
* equivalent and ignores it.
|
|
46
|
+
*/
|
|
47
|
+
startSession(alertMessage: string): Promise<EmrtdTagInfo>;
|
|
48
|
+
|
|
49
|
+
/** End the session. Safe to call when none is open. */
|
|
50
|
+
stopSession(message: string): Promise<void>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Update the live session's user-facing message.
|
|
54
|
+
*
|
|
55
|
+
* iOS narrates the read on its system sheet — which covers the app, so this
|
|
56
|
+
* is the ONLY feedback an iOS user gets between "hold your phone here" and
|
|
57
|
+
* the result. Android has no system NFC UI and ignores it (the SDK's own
|
|
58
|
+
* screen narrates there). Safe to call when no session is open.
|
|
59
|
+
*/
|
|
60
|
+
updateSessionMessage(message: string): Promise<void>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Send one APDU and return the response.
|
|
64
|
+
*
|
|
65
|
+
* A transport error rejects; a chip that answers with a non-success status
|
|
66
|
+
* word RESOLVES with that word, because those are protocol answers the
|
|
67
|
+
* caller must interpret rather than failures.
|
|
68
|
+
*/
|
|
69
|
+
transceive(apduBase64: string): Promise<EmrtdApduResponse>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Decode an image the JS side cannot render, returning JPEG bytes.
|
|
73
|
+
*
|
|
74
|
+
* The one that matters is JPEG 2000: most passport chips store their portrait
|
|
75
|
+
* (DG2) that way and React Native has no decoder for it, so the photo the
|
|
76
|
+
* issuing government put on the chip could be read and verified but never
|
|
77
|
+
* shown. The platform does have one — iOS registers `public.jpeg-2000` with
|
|
78
|
+
* ImageIO, Android with BitmapFactory.
|
|
79
|
+
*
|
|
80
|
+
* Returns an EMPTY STRING rather than null when the format is not understood
|
|
81
|
+
* — Nitro maps a nullable string to a variant type, which costs both native
|
|
82
|
+
* sides an unwrapping dance for a value whose only meaning is "no picture".
|
|
83
|
+
* Never throws: the portrait is a courtesy preview and the chip read stands on
|
|
84
|
+
* its own without it.
|
|
85
|
+
*/
|
|
86
|
+
decodeImage(dataBase64: string): Promise<string>;
|
|
87
|
+
|
|
88
|
+
// ── Crypto primitives ─────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/** SHA-1 digest, base64 in and out. */
|
|
91
|
+
sha1(dataBase64: string): string;
|
|
92
|
+
/** SHA-256 digest. */
|
|
93
|
+
sha256(dataBase64: string): string;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Two-key 3DES (K1|K2|K1) in CBC mode.
|
|
97
|
+
*
|
|
98
|
+
* `ivBase64` is empty for the zero IV that BAC uses. No padding is applied —
|
|
99
|
+
* ISO 9797-1 method 2 padding is the protocol's job, not the cipher's.
|
|
100
|
+
*/
|
|
101
|
+
desEde2Cbc(keyBase64: string, dataBase64: string, ivBase64: string, encrypt: boolean): string;
|
|
102
|
+
|
|
103
|
+
/** Single DES on ONE block — the retail MAC's inner primitive. */
|
|
104
|
+
desBlock(keyBase64: string, blockBase64: string, encrypt: boolean): string;
|
|
105
|
+
|
|
106
|
+
/** AES-CBC, for the AES cipher suites PACE may negotiate. */
|
|
107
|
+
aesCbc(keyBase64: string, dataBase64: string, ivBase64: string, encrypt: boolean): string;
|
|
108
|
+
|
|
109
|
+
/** AES-CMAC (RFC 4493), truncated by the caller as ICAO specifies. */
|
|
110
|
+
aesCmac(keyBase64: string, dataBase64: string): string;
|
|
111
|
+
|
|
112
|
+
/** Cryptographically secure random bytes — the BAC challenge depends on it. */
|
|
113
|
+
randomBytes(length: number): string;
|
|
114
|
+
}
|
|
@@ -32,6 +32,16 @@ export interface FaceResult {
|
|
|
32
32
|
faceSizeRatio: number; // face width / frame width (0–1)
|
|
33
33
|
faceCount: number; // # faces in frame; 0 = no face
|
|
34
34
|
brightness: number; // mean luma of the frame (0–255), for the low-light gate
|
|
35
|
+
faceCenterX: number; // face centre X, normalised 0–1 (−1 when unavailable)
|
|
36
|
+
faceCenterY: number; // face centre Y, normalised 0–1 (−1 when unavailable)
|
|
37
|
+
trackingId: number; // ML Kit per-face id; −1 on iOS / when untracked
|
|
38
|
+
// Mean RGB of the FACE REGION (0–255 each), for flash (screen-reflection)
|
|
39
|
+
// liveness. Sampled in this same pass because the pixel buffer is already
|
|
40
|
+
// mapped here — a second native module would mean a second frame walk for
|
|
41
|
+
// numbers we can take for free. −1 when no face was found.
|
|
42
|
+
faceR: number;
|
|
43
|
+
faceG: number;
|
|
44
|
+
faceB: number;
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
export interface MyazaFaceDetector extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> {
|