@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,121 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import * as ImagePicker from 'expo-image-picker';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
isAcceptedPoaMimeType,
|
|
6
|
+
POA_ACCEPTED_MIME_TYPES,
|
|
7
|
+
POA_MAX_BYTES,
|
|
8
|
+
} from '../config/proofOfAddress';
|
|
9
|
+
import { loadDocumentPicker } from '../services/documentPicker';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Getting a proof-of-address document off the device.
|
|
13
|
+
//
|
|
14
|
+
// One drop zone, like the web SDK — tapping it asks WHERE the document is
|
|
15
|
+
// coming from. Photographing a bill on the table and choosing a downloaded PDF
|
|
16
|
+
// are still different pickers, but they're a choice made after the user has
|
|
17
|
+
// committed to uploading, rather than two competing buttons that make the step
|
|
18
|
+
// look like it wants two files.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/** What the picker handed back, so the screen can preview it. */
|
|
22
|
+
export interface PoaPick {
|
|
23
|
+
uri: string;
|
|
24
|
+
mimeType: string | undefined;
|
|
25
|
+
name: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type UploadFn = (pick: PoaPick) => Promise<void>;
|
|
29
|
+
|
|
30
|
+
export function usePoaAttach(
|
|
31
|
+
upload: UploadFn,
|
|
32
|
+
setError: (message: string) => void,
|
|
33
|
+
): {
|
|
34
|
+
pick: () => void;
|
|
35
|
+
sheetOpen: boolean;
|
|
36
|
+
closeSheet: () => void;
|
|
37
|
+
choosePhoto: () => Promise<void>;
|
|
38
|
+
takePhoto: () => Promise<void>;
|
|
39
|
+
chooseFile: () => Promise<void>;
|
|
40
|
+
} {
|
|
41
|
+
const tooLarge = useCallback(
|
|
42
|
+
(size: number | undefined): boolean => {
|
|
43
|
+
if (typeof size === 'number' && size > POA_MAX_BYTES) {
|
|
44
|
+
setError('File is too large (max 20MB).');
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
},
|
|
49
|
+
[setError],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const takePhoto = useCallback(async () => {
|
|
53
|
+
const permission = await ImagePicker.requestCameraPermissionsAsync();
|
|
54
|
+
if (!permission.granted) {
|
|
55
|
+
setError('Camera access is needed to photograph the document.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const result = await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], quality: 1 });
|
|
59
|
+
const asset = result.canceled ? undefined : result.assets[0];
|
|
60
|
+
if (!asset) return;
|
|
61
|
+
if (tooLarge(asset.fileSize)) return;
|
|
62
|
+
await upload({
|
|
63
|
+
uri: asset.uri,
|
|
64
|
+
mimeType: asset.mimeType ?? 'image/jpeg',
|
|
65
|
+
name: asset.fileName ?? 'proof-of-address.jpg',
|
|
66
|
+
});
|
|
67
|
+
}, [setError, tooLarge, upload]);
|
|
68
|
+
|
|
69
|
+
const choosePhoto = useCallback(async () => {
|
|
70
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
71
|
+
mediaTypes: ['images'],
|
|
72
|
+
quality: 1,
|
|
73
|
+
});
|
|
74
|
+
const asset = result.canceled ? undefined : result.assets[0];
|
|
75
|
+
if (!asset) return;
|
|
76
|
+
if (tooLarge(asset.fileSize)) return;
|
|
77
|
+
await upload({
|
|
78
|
+
uri: asset.uri,
|
|
79
|
+
mimeType: asset.mimeType ?? 'image/jpeg',
|
|
80
|
+
name: asset.fileName ?? 'proof-of-address.jpg',
|
|
81
|
+
});
|
|
82
|
+
}, [tooLarge, upload]);
|
|
83
|
+
|
|
84
|
+
const chooseFile = useCallback(async () => {
|
|
85
|
+
const picker = loadDocumentPicker();
|
|
86
|
+
if (!picker) {
|
|
87
|
+
setError('Choosing a file is not available in this app. Please photograph the document.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const result = await picker.getDocumentAsync({
|
|
91
|
+
type: [...POA_ACCEPTED_MIME_TYPES],
|
|
92
|
+
copyToCacheDirectory: true,
|
|
93
|
+
multiple: false,
|
|
94
|
+
});
|
|
95
|
+
const asset = result.canceled ? undefined : result.assets?.[0];
|
|
96
|
+
if (!asset) return;
|
|
97
|
+
// The picker's `type` filter is advisory on some platforms, so what came
|
|
98
|
+
// back is re-checked rather than trusted.
|
|
99
|
+
if (!isAcceptedPoaMimeType(asset.mimeType)) {
|
|
100
|
+
setError('Please choose a photo (JPEG, PNG or WebP) or a PDF.');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (tooLarge(asset.size)) return;
|
|
104
|
+
await upload({
|
|
105
|
+
uri: asset.uri,
|
|
106
|
+
mimeType: asset.mimeType,
|
|
107
|
+
name: asset.name ?? 'proof-of-address',
|
|
108
|
+
});
|
|
109
|
+
}, [setError, tooLarge, upload]);
|
|
110
|
+
|
|
111
|
+
// The "where is it?" question is the SDK's own sheet now, not
|
|
112
|
+
// ActionSheetIOS / Alert — both were stock OS dialogs dropped into the
|
|
113
|
+
// middle of a branded flow (and the Android alert didn't even read as a
|
|
114
|
+
// chooser). The hook only owns the STATE; the screen renders the
|
|
115
|
+
// MediaSourceSheet, because a hook can't render.
|
|
116
|
+
const [sheetOpen, setSheetOpen] = useState(false);
|
|
117
|
+
const pick = useCallback(() => setSheetOpen(true), []);
|
|
118
|
+
const closeSheet = useCallback(() => setSheetOpen(false), []);
|
|
119
|
+
|
|
120
|
+
return { pick, sheetOpen, closeSheet, choosePhoto, takePhoto, chooseFile };
|
|
121
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// The HTTP contract: every request and response shape the SDK exchanges with
|
|
3
|
+
// the server.
|
|
4
|
+
//
|
|
5
|
+
// Split from api.ts (200-line rule) so the client and the contract can be read
|
|
6
|
+
// separately — and so a module that only needs a type does not pull in the
|
|
7
|
+
// fetch/blob machinery. The server is the source of truth for all of it; these
|
|
8
|
+
// are declarations, never validation.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
// The /verify request is large enough to live on its own; re-exported so this
|
|
12
|
+
// module remains the single view of the contract.
|
|
13
|
+
export type * from './api-verify-types';
|
|
14
|
+
|
|
15
|
+
/** Media kinds the SDK can upload — document/selfie photos plus best-effort videos. */
|
|
16
|
+
export type MediaUploadType =
|
|
17
|
+
| 'document_front'
|
|
18
|
+
| 'document_back'
|
|
19
|
+
| 'selfie'
|
|
20
|
+
| 'document_front_video'
|
|
21
|
+
| 'document_back_video'
|
|
22
|
+
| 'liveness_video'
|
|
23
|
+
// The two kinds that accept a PDF — a proof of address is usually a
|
|
24
|
+
// downloaded statement, and company paperwork is almost always a scan.
|
|
25
|
+
| 'proof_of_address'
|
|
26
|
+
| 'business_document';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A file to upload, in React Native's multipart shape. `uri` points at a
|
|
30
|
+
* local file (camera capture / gallery pick); `name`/`type` populate the
|
|
31
|
+
* multipart part's filename + Content-Type.
|
|
32
|
+
*/
|
|
33
|
+
export interface UploadFile {
|
|
34
|
+
uri: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
/** Raw mime type (codec params are stripped before sending). */
|
|
37
|
+
type?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface UploadResponse {
|
|
41
|
+
mediaId: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface KeyPersonInvite {
|
|
45
|
+
keyPersonId: string;
|
|
46
|
+
name: string;
|
|
47
|
+
inviteUrl: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface VerifyResponse {
|
|
51
|
+
verificationId: string;
|
|
52
|
+
status: 'pending';
|
|
53
|
+
/**
|
|
54
|
+
* KYB with applicant verification: the KeyPerson row the applicant's OWN
|
|
55
|
+
* identity check must link back to, via `metadata.userId` on a second,
|
|
56
|
+
* ordinary individual submission.
|
|
57
|
+
*/
|
|
58
|
+
applicantKeyPersonId?: string;
|
|
59
|
+
/** Per-person hosted-KYC links to hand to the people the applicant listed. */
|
|
60
|
+
keyPeopleInvites?: KeyPersonInvite[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Minimal, publishable-safe status from `GET /api/kyc/status/:id` — no PII,
|
|
65
|
+
* scores, or result data; only the lifecycle state and an org-safe reason.
|
|
66
|
+
*/
|
|
67
|
+
export interface VerificationStatusResponse {
|
|
68
|
+
verificationId: string;
|
|
69
|
+
status: 'pending' | 'verified' | 'failed' | 'not_found' | 'error';
|
|
70
|
+
reason?: string | null;
|
|
71
|
+
reasonCode?: string | null;
|
|
72
|
+
createdAt: string;
|
|
73
|
+
completedAt?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SdkConfigIdType {
|
|
77
|
+
country: string;
|
|
78
|
+
idType: string;
|
|
79
|
+
/**
|
|
80
|
+
* Whether this document carries a readable eMRTD chip. Catalogue-driven
|
|
81
|
+
* server-side and intrinsic to the ID (not org-gated), so it is the
|
|
82
|
+
* authoritative answer to "should the chip step be offered".
|
|
83
|
+
*/
|
|
84
|
+
supportsNfc?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Display metadata the server sends per pair. Load-bearing for Global
|
|
87
|
+
* Documents: the SDK's curated table only covers the five gov-DB countries,
|
|
88
|
+
* so for any of the other ~235 these fields ARE the definition. Dropping
|
|
89
|
+
* them (as this type used to) left those countries with an empty ID list.
|
|
90
|
+
*/
|
|
91
|
+
label?: string;
|
|
92
|
+
requiresDocumentCapture?: boolean;
|
|
93
|
+
scanSides?: string;
|
|
94
|
+
features: {
|
|
95
|
+
documentVerification: boolean;
|
|
96
|
+
livenessCheck: boolean;
|
|
97
|
+
govDbCheck: boolean;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Org branding configured server-side, returned with the SDK config. */
|
|
102
|
+
export interface SdkConfigBranding {
|
|
103
|
+
logo?: string;
|
|
104
|
+
companyName?: string;
|
|
105
|
+
primaryColor?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SdkConfigResponse {
|
|
109
|
+
environment: 'DEVELOPMENT' | 'SANDBOX' | 'PRODUCTION';
|
|
110
|
+
idTypes: SdkConfigIdType[];
|
|
111
|
+
branding?: SdkConfigBranding;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The template half of a resolved workflow — exactly the keys a published flow
|
|
116
|
+
* may set. Loosely typed on purpose: the server is the contract, and an unknown
|
|
117
|
+
* key added there must not make an older SDK fail to parse. `mergeWorkflowConfig`
|
|
118
|
+
* only reads the keys it knows.
|
|
119
|
+
*/
|
|
120
|
+
export type WorkflowConfigPayload = Record<string, unknown>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A KYB workflow's mapped APPLICANT workflow (business.applicant.workflowId),
|
|
124
|
+
* resolved server-side: the individual workflow whose capture template
|
|
125
|
+
* overlays the applicant's own KYC leg, and whose id is stamped on that
|
|
126
|
+
* submission.
|
|
127
|
+
*/
|
|
128
|
+
export interface ApplicantWorkflowPayload {
|
|
129
|
+
id: string;
|
|
130
|
+
name: string;
|
|
131
|
+
version: number;
|
|
132
|
+
config: WorkflowConfigPayload;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Response from `GET /api/kyc/workflows/:workflowId` — one round trip hydrates
|
|
136
|
+
* the SDK, so `/config` is skipped entirely. */
|
|
137
|
+
export interface WorkflowResolutionResponse {
|
|
138
|
+
workflow: { id: string; name: string; version: number };
|
|
139
|
+
config: WorkflowConfigPayload;
|
|
140
|
+
environment: 'DEVELOPMENT' | 'SANDBOX' | 'PRODUCTION';
|
|
141
|
+
/** Org allowlist + per-ID feature flags (same shape as /config). */
|
|
142
|
+
idTypes: SdkConfigIdType[];
|
|
143
|
+
branding?: SdkConfigBranding;
|
|
144
|
+
/** KYB only: the mapped applicant workflow, when configured and resolvable. */
|
|
145
|
+
applicantWorkflow?: ApplicantWorkflowPayload | null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface ContactSendResponse {
|
|
149
|
+
challengeId: string;
|
|
150
|
+
expiresAt: string;
|
|
151
|
+
deliveryChannel: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface ContactCheckResponse {
|
|
155
|
+
verified: boolean;
|
|
156
|
+
/** Single-use proof submitted with /verify as `contact.emailToken`/`phoneToken`. */
|
|
157
|
+
token: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface HealthResponse {
|
|
161
|
+
status: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The mime types the server accepts (must mirror the server's upload allowlist).
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// The /verify request.
|
|
3
|
+
//
|
|
4
|
+
// Split from api-types.ts (200-line rule) because it is the largest and most
|
|
5
|
+
// consequential shape in the contract: every optional block on it corresponds
|
|
6
|
+
// to a step the flow may or may not have run, and sending one the flow did not
|
|
7
|
+
// collect is a claim we cannot support.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export interface VerifyRequest {
|
|
11
|
+
country: string;
|
|
12
|
+
idType: string;
|
|
13
|
+
idNumber?: string;
|
|
14
|
+
/** The org's user reference → Entity.externalUserId at the seam (not matched). */
|
|
15
|
+
userId?: string;
|
|
16
|
+
userData?: {
|
|
17
|
+
firstName?: string;
|
|
18
|
+
lastName?: string;
|
|
19
|
+
dateOfBirth?: string;
|
|
20
|
+
};
|
|
21
|
+
mediaIds: {
|
|
22
|
+
documentFront?: string;
|
|
23
|
+
documentBack?: string;
|
|
24
|
+
selfie?: string;
|
|
25
|
+
documentFrontVideo?: string;
|
|
26
|
+
documentBackVideo?: string;
|
|
27
|
+
livenessVideo?: string;
|
|
28
|
+
proofOfAddress?: string;
|
|
29
|
+
};
|
|
30
|
+
/** Which kind of document the user said they uploaded as proof of address. */
|
|
31
|
+
proofOfAddressType?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Business (KYB) registry details. Its presence is what makes this a business
|
|
34
|
+
* submission — and the server REQUIRES a published KYB workflow for one, so
|
|
35
|
+
* this never appears without a `workflowId`.
|
|
36
|
+
*/
|
|
37
|
+
business?: {
|
|
38
|
+
registrationNumber: string;
|
|
39
|
+
registrationName?: string;
|
|
40
|
+
product?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Contact email for key-people verification — the server emails this
|
|
43
|
+
* address the invite links when the workflow's `keyPeople.invite.channel`
|
|
44
|
+
* is 'email' and a role needs full KYC. Sent only when non-empty.
|
|
45
|
+
*/
|
|
46
|
+
contactEmail?: string;
|
|
47
|
+
/** Company profile (the collectCompanyInfo fields) — echoed on the org's
|
|
48
|
+
* webhook and address-matched against the registry. */
|
|
49
|
+
address?: string;
|
|
50
|
+
email?: string;
|
|
51
|
+
phone?: string;
|
|
52
|
+
website?: string;
|
|
53
|
+
/** Uploaded supporting documents (honoured only when the workflow's
|
|
54
|
+
* `business.documents` block configures them). */
|
|
55
|
+
documents?: Array<{ type: string; mediaId: string }>;
|
|
56
|
+
/** Applicant-declared directors & owners (≤20; `email` drives auto-sent
|
|
57
|
+
* invites; honoured only when the workflow sets `keyPeople.collect`). */
|
|
58
|
+
keyPeople?: Array<{
|
|
59
|
+
name: string;
|
|
60
|
+
role: string;
|
|
61
|
+
email?: string;
|
|
62
|
+
/** The person's ISO-2 country — drives their verification link's country. */
|
|
63
|
+
country?: string;
|
|
64
|
+
ownershipPct?: number;
|
|
65
|
+
/** This entry IS the applicant (picked on the applicant-role step) —
|
|
66
|
+
* the server merges it with the applicant row: one person, one KYC,
|
|
67
|
+
* one screening, no invite. */
|
|
68
|
+
isApplicant?: boolean;
|
|
69
|
+
}>;
|
|
70
|
+
/** The applicant's declared role (+ optional name — the server backfills it
|
|
71
|
+
* from their verified KYC when absent). */
|
|
72
|
+
applicant?: { role: string; name?: string };
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* eMRTD chip data, base64. The server runs PASSIVE AUTHENTICATION on it —
|
|
76
|
+
* hashing DG1 against the signed security object and verifying the document
|
|
77
|
+
* signer — and only the server may conclude a chip is genuine. `chipAuth`
|
|
78
|
+
* reports how the chip was unlocked and is informational.
|
|
79
|
+
*/
|
|
80
|
+
nfc?: { dg1: string; sod?: string; dg2?: string; chipAuth?: string };
|
|
81
|
+
/** The workflow that drove this flow — attributes the submission to it. */
|
|
82
|
+
workflowId?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Single-use proofs from the contact-verification steps. The server drops an
|
|
85
|
+
* invalid or expired proof; a workflow that REQUIRES one 422s without it.
|
|
86
|
+
*/
|
|
87
|
+
contact?: { emailToken?: string; phoneToken?: string };
|
|
88
|
+
/**
|
|
89
|
+
* Compliance answers, validated server-side against the PUBLISHED
|
|
90
|
+
* questionnaire definition. Sent without a workflow they are discarded —
|
|
91
|
+
* there is no definition to trust them against.
|
|
92
|
+
*/
|
|
93
|
+
questionnaire?: Record<string, string | number | boolean | string[]>;
|
|
94
|
+
metadata: {
|
|
95
|
+
requestId: string;
|
|
96
|
+
device?: Record<string, unknown>;
|
|
97
|
+
[key: string]: unknown;
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/services/api.ts
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import { SDK_VERSION } from './deviceMetadata';
|
|
2
|
+
import type {
|
|
3
|
+
ContactCheckResponse,
|
|
4
|
+
ContactSendResponse,
|
|
5
|
+
HealthResponse,
|
|
6
|
+
MediaUploadType,
|
|
7
|
+
SdkConfigResponse,
|
|
8
|
+
UploadFile,
|
|
9
|
+
UploadResponse,
|
|
10
|
+
VerificationStatusResponse,
|
|
11
|
+
VerifyRequest,
|
|
12
|
+
VerifyResponse,
|
|
13
|
+
WorkflowResolutionResponse,
|
|
14
|
+
} from './api-types';
|
|
15
|
+
|
|
16
|
+
// The HTTP contract lives in ./api-types and is re-exported here, so importers
|
|
17
|
+
// keep a single entry point for both the client and the shapes it exchanges.
|
|
18
|
+
export * from './api-types';
|
|
2
19
|
|
|
3
20
|
// ---------------------------------------------------------------------------
|
|
4
21
|
// HTTP client — talks to the only endpoints the SDK calls (upload / verify /
|
|
@@ -80,107 +97,9 @@ async function handleResponse<T>(res: Response): Promise<T> {
|
|
|
80
97
|
return data as T;
|
|
81
98
|
}
|
|
82
99
|
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// Types
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
/** Media kinds the SDK can upload — document/selfie photos plus best-effort videos. */
|
|
88
|
-
export type MediaUploadType =
|
|
89
|
-
| 'document_front'
|
|
90
|
-
| 'document_back'
|
|
91
|
-
| 'selfie'
|
|
92
|
-
| 'document_front_video'
|
|
93
|
-
| 'document_back_video'
|
|
94
|
-
| 'liveness_video';
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* A file to upload, in React Native's multipart shape. `uri` points at a
|
|
98
|
-
* local file (camera capture / gallery pick); `name`/`type` populate the
|
|
99
|
-
* multipart part's filename + Content-Type.
|
|
100
|
-
*/
|
|
101
|
-
export interface UploadFile {
|
|
102
|
-
uri: string;
|
|
103
|
-
name?: string;
|
|
104
|
-
/** Raw mime type (codec params are stripped before sending). */
|
|
105
|
-
type?: string;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface UploadResponse {
|
|
109
|
-
mediaId: string;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export interface VerifyRequest {
|
|
113
|
-
country: string;
|
|
114
|
-
idType: string;
|
|
115
|
-
idNumber?: string;
|
|
116
|
-
userData?: {
|
|
117
|
-
firstName?: string;
|
|
118
|
-
lastName?: string;
|
|
119
|
-
dateOfBirth?: string;
|
|
120
|
-
};
|
|
121
|
-
mediaIds: {
|
|
122
|
-
documentFront?: string;
|
|
123
|
-
documentBack?: string;
|
|
124
|
-
selfie?: string;
|
|
125
|
-
documentFrontVideo?: string;
|
|
126
|
-
documentBackVideo?: string;
|
|
127
|
-
livenessVideo?: string;
|
|
128
|
-
};
|
|
129
|
-
metadata: {
|
|
130
|
-
requestId: string;
|
|
131
|
-
device?: Record<string, unknown>;
|
|
132
|
-
[key: string]: unknown;
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export interface VerifyResponse {
|
|
137
|
-
verificationId: string;
|
|
138
|
-
status: 'pending';
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Minimal, publishable-safe status from `GET /api/kyc/status/:id` — no PII,
|
|
143
|
-
* scores, or result data; only the lifecycle state and an org-safe reason.
|
|
144
|
-
*/
|
|
145
|
-
export interface VerificationStatusResponse {
|
|
146
|
-
verificationId: string;
|
|
147
|
-
status: 'pending' | 'verified' | 'failed' | 'not_found' | 'error';
|
|
148
|
-
reason?: string | null;
|
|
149
|
-
reasonCode?: string | null;
|
|
150
|
-
createdAt: string;
|
|
151
|
-
completedAt?: string;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export interface SdkConfigIdType {
|
|
155
|
-
country: string;
|
|
156
|
-
idType: string;
|
|
157
|
-
features: {
|
|
158
|
-
documentVerification: boolean;
|
|
159
|
-
livenessCheck: boolean;
|
|
160
|
-
govDbCheck: boolean;
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Org branding configured server-side, returned with the SDK config. */
|
|
165
|
-
export interface SdkConfigBranding {
|
|
166
|
-
logo?: string;
|
|
167
|
-
companyName?: string;
|
|
168
|
-
primaryColor?: string;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export interface SdkConfigResponse {
|
|
172
|
-
environment: 'DEVELOPMENT' | 'SANDBOX' | 'PRODUCTION';
|
|
173
|
-
idTypes: SdkConfigIdType[];
|
|
174
|
-
branding?: SdkConfigBranding;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export interface HealthResponse {
|
|
178
|
-
status: string;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// The mime types the server accepts (must mirror the server's upload allowlist).
|
|
182
100
|
const IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp'] as const;
|
|
183
101
|
const VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'] as const;
|
|
102
|
+
const POA_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'] as const;
|
|
184
103
|
|
|
185
104
|
const MIME_EXTENSIONS: Record<string, string> = {
|
|
186
105
|
'image/jpeg': 'jpg',
|
|
@@ -188,6 +107,7 @@ const MIME_EXTENSIONS: Record<string, string> = {
|
|
|
188
107
|
'image/webp': 'webp',
|
|
189
108
|
'video/webm': 'webm',
|
|
190
109
|
'video/mp4': 'mp4',
|
|
110
|
+
'application/pdf': 'pdf',
|
|
191
111
|
};
|
|
192
112
|
|
|
193
113
|
/**
|
|
@@ -199,7 +119,12 @@ const MIME_EXTENSIONS: Record<string, string> = {
|
|
|
199
119
|
function normalizeMimeType(rawType: string | undefined, type: MediaUploadType): string {
|
|
200
120
|
const base = (rawType?.split(';')[0] ?? '').trim().toLowerCase();
|
|
201
121
|
const isVideo = type.endsWith('_video');
|
|
202
|
-
const
|
|
122
|
+
const isPoa = type === 'proof_of_address' || type === 'business_document';
|
|
123
|
+
const allowed: readonly string[] = isVideo
|
|
124
|
+
? VIDEO_MIME_TYPES
|
|
125
|
+
: isPoa
|
|
126
|
+
? POA_MIME_TYPES
|
|
127
|
+
: IMAGE_MIME_TYPES;
|
|
203
128
|
if (allowed.includes(base)) return base;
|
|
204
129
|
return isVideo ? 'video/mp4' : 'image/jpeg';
|
|
205
130
|
}
|
|
@@ -277,8 +202,52 @@ export function createKYCApi(baseUrl: string, apiKey: string) {
|
|
|
277
202
|
return request<VerificationStatusResponse>(`/status/${verificationId}`);
|
|
278
203
|
},
|
|
279
204
|
|
|
280
|
-
|
|
281
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Send a contact-verification OTP. Non-production environments never
|
|
207
|
+
* deliver a real message — the code is all zeros of `codeLength`, so an
|
|
208
|
+
* integrator can automate the flow for free.
|
|
209
|
+
*/
|
|
210
|
+
async contactSend(body: {
|
|
211
|
+
channel: 'email' | 'phone';
|
|
212
|
+
destination: string;
|
|
213
|
+
/** ISO-2 default country for national phone formats (the flow's country). */
|
|
214
|
+
country?: string;
|
|
215
|
+
/** Phone delivery channel preference (default sms). */
|
|
216
|
+
via?: 'sms' | 'whatsapp';
|
|
217
|
+
/** Org-configured code length (server clamps 4–8). */
|
|
218
|
+
codeLength?: number;
|
|
219
|
+
/** Org-configured attempt budget (server clamps 1–5). */
|
|
220
|
+
maxAttempts?: number;
|
|
221
|
+
}): Promise<ContactSendResponse> {
|
|
222
|
+
return request<ContactSendResponse>('/contact/send', {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
body: JSON.stringify(body),
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
/** Check a typed code. The returned `token` is a single-use proof that
|
|
229
|
+
* rides the /verify submission. */
|
|
230
|
+
async contactCheck(body: { challengeId: string; code: string }): Promise<ContactCheckResponse> {
|
|
231
|
+
return request<ContactCheckResponse>('/contact/check', {
|
|
232
|
+
method: 'POST',
|
|
233
|
+
body: JSON.stringify(body),
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
async config(signal?: AbortSignal): Promise<SdkConfigResponse> {
|
|
238
|
+
return request<SdkConfigResponse>('/config', signal ? { signal } : {});
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Resolve a published workflow. Returns the flow's config plus the org's
|
|
243
|
+
* ID-type allowlist and branding, so a `workflowId` mount needs no
|
|
244
|
+
* subsequent `/config` call.
|
|
245
|
+
*/
|
|
246
|
+
async workflow(workflowId: string, signal?: AbortSignal): Promise<WorkflowResolutionResponse> {
|
|
247
|
+
return request<WorkflowResolutionResponse>(
|
|
248
|
+
`/workflows/${encodeURIComponent(workflowId)}`,
|
|
249
|
+
signal ? { signal } : {},
|
|
250
|
+
);
|
|
282
251
|
},
|
|
283
252
|
|
|
284
253
|
async health(): Promise<HealthResponse> {
|
|
@@ -289,3 +258,4 @@ export function createKYCApi(baseUrl: string, apiKey: string) {
|
|
|
289
258
|
}
|
|
290
259
|
|
|
291
260
|
export type KYCApi = ReturnType<typeof createKYCApi>;
|
|
261
|
+
|
package/src/services/cardCrop.ts
CHANGED
|
@@ -12,36 +12,51 @@ export interface CropRect {
|
|
|
12
12
|
height: number;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/** Legacy framed-viewfinder aspect — only correct when capture runs in the 3:4 box. */
|
|
16
|
+
export const FRAMED_VIEW_AR = 3 / 4;
|
|
16
17
|
const GUIDE_WIDTH_FRACTION = 0.88; // card width as a fraction of the box (overlay GW/VB_W)
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
|
-
* Given the photo's pixel size
|
|
20
|
-
* the
|
|
21
|
-
*
|
|
20
|
+
* Given the photo's pixel size, the guide `aspect` (width ÷ height), and the
|
|
21
|
+
* aspect of the box the preview ACTUALLY rendered in, return the card-guide
|
|
22
|
+
* crop rect in photo pixels:
|
|
23
|
+
* 1. the cover-fit box shows a centred `viewAr` slice of the photo,
|
|
22
24
|
* 2. the guide is centred in that slice (88% wide, `aspect` ratio).
|
|
25
|
+
*
|
|
26
|
+
* `viewAr` is a parameter because assuming it was the bug: this math was
|
|
27
|
+
* written for the framed 3:4 viewfinder, and capture then moved to a
|
|
28
|
+
* FULL-SCREEN modal (~9:19.5) without the constant following. A cover-fit
|
|
29
|
+
* 3:4 slice of the photo is far wider than the slice a full-screen preview
|
|
30
|
+
* shows, so every review image came back ~1.6× wider than what the user had
|
|
31
|
+
* framed. The caller passes the MEASURED box aspect, so the crop and the
|
|
32
|
+
* preview agree by construction.
|
|
23
33
|
*/
|
|
24
|
-
export function cardCropRect(
|
|
34
|
+
export function cardCropRect(
|
|
35
|
+
imageW: number,
|
|
36
|
+
imageH: number,
|
|
37
|
+
aspect: number,
|
|
38
|
+
viewAr: number = FRAMED_VIEW_AR,
|
|
39
|
+
): CropRect {
|
|
25
40
|
const photoAR = imageW / imageH;
|
|
26
41
|
let visW: number;
|
|
27
42
|
let visH: number;
|
|
28
43
|
let offX: number;
|
|
29
44
|
let offY: number;
|
|
30
|
-
if (photoAR >
|
|
45
|
+
if (photoAR > viewAr) {
|
|
31
46
|
visH = imageH;
|
|
32
|
-
visW = imageH *
|
|
47
|
+
visW = imageH * viewAr;
|
|
33
48
|
offX = (imageW - visW) / 2;
|
|
34
49
|
offY = 0;
|
|
35
50
|
} else {
|
|
36
51
|
visW = imageW;
|
|
37
|
-
visH = imageW /
|
|
52
|
+
visH = imageW / viewAr;
|
|
38
53
|
offX = 0;
|
|
39
54
|
offY = (imageH - visH) / 2;
|
|
40
55
|
}
|
|
41
56
|
const leftFraction = (1 - GUIDE_WIDTH_FRACTION) / 2;
|
|
42
57
|
// card height ÷ box height = (cardWidthFrac · boxWidth / aspect) / boxHeight,
|
|
43
|
-
// and boxWidth ÷ boxHeight =
|
|
44
|
-
const heightFraction = (GUIDE_WIDTH_FRACTION *
|
|
58
|
+
// and boxWidth ÷ boxHeight = viewAr.
|
|
59
|
+
const heightFraction = (GUIDE_WIDTH_FRACTION * viewAr) / aspect;
|
|
45
60
|
const topFraction = (1 - heightFraction) / 2;
|
|
46
61
|
return {
|
|
47
62
|
originX: offX + leftFraction * visW,
|