@myazahq/kyc-sdk-react-native 2.4.0 → 2.6.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 +151 -5
- package/android/build.gradle +35 -3
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +77 -0
- package/app.plugin.js +89 -8
- package/ios/HybridMyazaFaceDetector.swift +16 -3
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
- package/package.json +14 -2
- package/src/MyazaBiometricAuth.tsx +119 -0
- package/src/MyazaKYC.tsx +12 -0
- package/src/components/CountryField.tsx +8 -0
- package/src/components/CountryRegionPicker.tsx +184 -0
- package/src/components/DashedBorder.tsx +55 -0
- package/src/components/DialCodePicker.tsx +53 -67
- package/src/components/DialCodeRow.tsx +102 -0
- package/src/components/FramedMapPicker.tsx +122 -0
- package/src/components/GeoBadge.tsx +34 -0
- package/src/components/Icon.tsx +2 -176
- package/src/components/KycFlow.tsx +60 -32
- package/src/components/LineSkeleton.tsx +100 -0
- package/src/components/MapChrome.tsx +67 -0
- package/src/components/MapPinMarker.tsx +54 -0
- package/src/components/MapPinPicker.tsx +179 -0
- package/src/components/MilestoneTrack.tsx +155 -0
- package/src/components/MultiIdProgress.tsx +107 -0
- package/src/components/MyazaInput.tsx +39 -4
- package/src/components/MyazaSelect.tsx +2 -1
- package/src/components/OptionRow.tsx +22 -3
- package/src/components/OwnershipSlider.tsx +102 -0
- package/src/components/PhoneNumberInput.tsx +45 -4
- package/src/components/PoweredBy.tsx +5 -5
- package/src/components/PresenceBlocks.tsx +153 -0
- package/src/components/StaggerIn.tsx +63 -0
- package/src/components/StepView.tsx +43 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/glass/FloatingSheet.tsx +73 -21
- package/src/components/glass/GlassSheet.tsx +14 -2
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +45 -5
- package/src/config/addressCollection.ts +125 -0
- package/src/config/biometricOptions.ts +100 -0
- package/src/config/business.ts +26 -7
- package/src/config/businessDetailsValidity.ts +52 -0
- package/src/config/businessPrefill.ts +72 -0
- package/src/config/businessSteps.ts +20 -3
- package/src/config/consentStep.ts +19 -0
- package/src/config/keyPeople.ts +111 -7
- package/src/config/keyPeopleCorporate.ts +19 -0
- package/src/config/keyPeopleOwnerTypes.ts +12 -0
- package/src/config/keyPeopleOwners.ts +26 -0
- package/src/config/keyPeoplePrefill.ts +138 -0
- package/src/config/keyPeopleSectionDefs.ts +65 -0
- package/src/config/keyPeopleSections.ts +167 -0
- package/src/config/proofOfAddress.ts +54 -11
- package/src/config/regions.ts +24 -0
- package/src/config/registrationHint.ts +1 -1
- package/src/config/stepOrder.ts +100 -6
- package/src/config/theme.ts +8 -2
- package/src/config/uploadLimits.ts +35 -0
- package/src/config/website.ts +36 -0
- package/src/config/workflowMerge.ts +16 -5
- package/src/emrtd/activeAuth.ts +100 -0
- package/src/emrtd/extras.ts +76 -0
- package/src/emrtd/files.ts +5 -0
- package/src/emrtd/index.ts +1 -0
- package/src/emrtd/read.ts +8 -1
- package/src/emrtd/session.ts +41 -0
- package/src/emrtd/stages.ts +11 -0
- package/src/index.ts +54 -1
- package/src/lib/address-current-location.ts +171 -0
- package/src/lib/address-field-modes.ts +119 -0
- package/src/lib/address-flow.ts +172 -0
- package/src/lib/address-helpers.ts +48 -0
- package/src/lib/address-line.ts +76 -0
- package/src/lib/address-pin-move.ts +97 -0
- package/src/lib/address-step-recovery.ts +63 -0
- package/src/lib/authed-image.ts +78 -0
- package/src/lib/biometric-auth.ts +52 -0
- package/src/lib/biometric-copy.ts +51 -0
- package/src/lib/captureRing.ts +83 -0
- package/src/lib/contact-recovery.ts +44 -0
- package/src/lib/country-adoption.ts +89 -0
- package/src/lib/inferred-country.ts +65 -0
- package/src/lib/livenessLayout.ts +49 -0
- package/src/lib/map-frame.ts +159 -0
- package/src/lib/map-tiles.ts +143 -0
- package/src/lib/multi-id.ts +210 -0
- package/src/lib/poa-country-gate.ts +27 -0
- package/src/lib/resubmit.ts +130 -0
- package/src/lib/result-copy.ts +116 -0
- package/src/lib/result-wait.ts +53 -0
- package/src/lib/review-map-surface.ts +26 -0
- package/src/lib/scope.ts +31 -0
- package/src/lib/screen-corners.ts +50 -0
- package/src/lib/selfie-upload-wait.ts +79 -0
- package/src/lib/step-log.ts +24 -3
- package/src/lib/street-view-fov.ts +42 -0
- package/src/lib/use-multi-id-plan.ts +29 -0
- package/src/lib/webview-available.ts +47 -0
- package/src/liveness/useLiveness.ts +6 -1
- package/src/liveness/useModelReady.ts +65 -0
- package/src/liveness/visionCameraFaceDetector.ts +35 -0
- package/src/presence/background-math.ts +100 -0
- package/src/presence/background-store.ts +82 -0
- package/src/presence/background.ts +165 -0
- package/src/presence/foreground-service.ts +193 -0
- package/src/presence/fs.ts +51 -0
- package/src/presence/geofence.ts +41 -0
- package/src/presence/math.ts +44 -0
- package/src/presence/post.ts +40 -0
- package/src/presence/report.ts +87 -0
- package/src/presence/sampler.ts +110 -0
- package/src/presence/status.ts +92 -0
- package/src/presence/store.ts +94 -0
- package/src/presence/tier.ts +36 -0
- package/src/presence/watch-wait.ts +81 -0
- package/src/screens/AddressCountryControl.tsx +115 -0
- package/src/screens/ApplicantRoleStep.tsx +9 -5
- package/src/screens/BusinessCheckPanel.tsx +89 -0
- package/src/screens/BusinessDetailsFields.tsx +112 -0
- package/src/screens/BusinessDetailsStep.tsx +133 -145
- package/src/screens/BusinessDocumentSlot.tsx +5 -5
- package/src/screens/BusinessDocumentsStep.tsx +5 -56
- package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
- package/src/screens/BusinessPickedCard.tsx +86 -0
- package/src/screens/BusinessPickedSection.tsx +51 -0
- package/src/screens/BusinessRegistryPickers.tsx +67 -0
- package/src/screens/BusinessSandboxToggle.tsx +114 -0
- package/src/screens/BusinessSearch.tsx +199 -0
- package/src/screens/BusinessSearchResults.tsx +115 -0
- package/src/screens/CompanyInfoFields.tsx +91 -51
- package/src/screens/ConsentStep.tsx +37 -17
- package/src/screens/ContactActions.tsx +54 -0
- package/src/screens/ContactDestinationField.tsx +5 -0
- package/src/screens/ContactEntryPanel.tsx +75 -0
- package/src/screens/ContactVerificationStep.tsx +43 -42
- package/src/screens/CountrySelectStep.tsx +13 -147
- package/src/screens/IdTypeStep.tsx +12 -5
- package/src/screens/KeepLinksSheet.tsx +88 -0
- package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
- package/src/screens/KeyPeopleAwaitList.tsx +126 -0
- package/src/screens/KeyPeoplePending.tsx +154 -0
- package/src/screens/KeyPeopleSection.tsx +162 -0
- package/src/screens/KeyPeopleSectionsList.tsx +103 -0
- package/src/screens/KeyPeopleUboExemption.tsx +72 -0
- package/src/screens/KeyPersonCard.tsx +36 -4
- package/src/screens/KeyPersonForm.tsx +195 -36
- package/src/screens/KeyPersonKindToggle.tsx +68 -0
- package/src/screens/KeyPersonOwners.tsx +109 -0
- package/src/screens/KeyPersonRoleChips.tsx +68 -0
- package/src/screens/KeyPersonSheet.tsx +92 -108
- package/src/screens/LivenessAvatar.tsx +15 -3
- package/src/screens/LivenessStep.tsx +172 -52
- package/src/screens/NfcStep.tsx +14 -0
- package/src/screens/PoaDocumentTypeList.tsx +66 -0
- package/src/screens/ProofOfAddressParts.tsx +151 -0
- package/src/screens/ProofOfAddressStep.tsx +58 -96
- package/src/screens/SubmittedBadge.tsx +25 -0
- package/src/screens/SubmittedError.tsx +64 -0
- package/src/screens/SubmittedResult.tsx +117 -0
- package/src/screens/SubmittedStep.tsx +82 -119
- package/src/screens/SubmittedSuccess.tsx +127 -0
- package/src/screens/SubmittedWaiting.tsx +45 -0
- package/src/screens/address/AddressEntranceStep.tsx +161 -0
- package/src/screens/address/AddressIntroGate.tsx +148 -0
- package/src/screens/address/AddressMapStub.tsx +50 -0
- package/src/screens/address/AddressPinStep.tsx +178 -0
- package/src/screens/address/AddressReviewStep.tsx +162 -0
- package/src/screens/address/AddressSandboxTabs.tsx +158 -0
- package/src/screens/address/AddressSearchField.tsx +115 -0
- package/src/screens/address/AddressSearchStep.tsx +94 -0
- package/src/screens/address/CurrentLocationRow.tsx +135 -0
- package/src/screens/address/DetailsSheet.tsx +175 -0
- package/src/screens/address/DetailsSheetFields.tsx +184 -0
- package/src/screens/address/EntranceDropzone.tsx +178 -0
- package/src/screens/address/EntranceFraming.tsx +66 -0
- package/src/screens/address/EntrancePills.tsx +60 -0
- package/src/screens/address/FramedStreetView.tsx +155 -0
- package/src/screens/address/IntroDisclosures.tsx +210 -0
- package/src/screens/address/LabelDecisionRow.tsx +113 -0
- package/src/screens/address/PinSummaryRow.tsx +105 -0
- package/src/screens/address/ReviewAddressBand.tsx +111 -0
- package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
- package/src/screens/address/ReviewMapPicture.tsx +106 -0
- package/src/screens/address/SearchResults.tsx +105 -0
- package/src/screens/address/SearchScreen.tsx +176 -0
- package/src/screens/address/SkipForNow.tsx +43 -0
- package/src/screens/address/StreetViewChrome.tsx +81 -0
- package/src/screens/address/detail-values.ts +22 -0
- package/src/screens/address/fix-source.ts +27 -0
- package/src/screens/address/index.ts +8 -0
- package/src/screens/address/meta.ts +44 -0
- package/src/screens/address/use-address-flow.ts +200 -0
- package/src/screens/address/use-label-pin.ts +80 -0
- package/src/screens/address/use-pin-actions.ts +192 -0
- package/src/screens/biometric/BiometricAuthFlow.tsx +170 -0
- package/src/screens/consent/model.ts +79 -10
- package/src/screens/liveness/CaptureRing.tsx +91 -0
- package/src/screens/liveness/LivenessCamera.tsx +63 -0
- package/src/screens/liveness/LivenessHandover.tsx +43 -0
- package/src/screens/liveness/LivenessOutcome.tsx +4 -3
- package/src/screens/liveness/SelfiePreview.tsx +13 -4
- package/src/screens/liveness/ShutterFlash.tsx +25 -0
- package/src/screens/liveness/index.ts +4 -0
- package/src/screens/liveness/useSelfieUpload.ts +23 -3
- package/src/screens/nfc/NfcReadProgress.tsx +1 -0
- package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
- package/src/screens/useAddressPhotoAttach.ts +77 -0
- package/src/screens/useAwaitingPeople.ts +90 -0
- package/src/screens/useBusinessDocumentAttach.ts +93 -0
- package/src/screens/usePoaAttach.ts +12 -15
- package/src/services/api-biometric.ts +37 -0
- package/src/services/api-types-biometric.ts +41 -0
- package/src/services/api-types.ts +251 -3
- package/src/services/api-verify-types.ts +90 -1
- package/src/services/api.ts +237 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/errors.ts +10 -2
- package/src/services/location.ts +234 -0
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +8 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
- package/src/store/address.ts +97 -0
- package/src/store/businessCheck.ts +113 -0
- package/src/store/derive.ts +44 -4
- package/src/store/kycStore.ts +352 -16
- package/src/store/serverConfig.ts +20 -0
- package/src/store/session.ts +227 -0
- package/src/store/state.ts +289 -2
- package/src/store/submit.ts +80 -13
- package/src/store/submitApplicant.ts +5 -11
- package/src/types/business.ts +23 -2
- package/src/types/config.ts +70 -2
- package/src/types/verification.ts +13 -1
- package/src/types/workflow.ts +112 -1
- package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
package/src/emrtd/read.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Platform } from 'react-native';
|
|
|
2
2
|
|
|
3
3
|
import { nativeModule } from './native';
|
|
4
4
|
import { readChip, type EmrtdReadResult, EmrtdSessionError } from './session';
|
|
5
|
+
import type { AaChallenge } from './activeAuth';
|
|
5
6
|
import {
|
|
6
7
|
MAX_CHIP_ATTEMPTS,
|
|
7
8
|
chipRetryDelayMs,
|
|
@@ -37,6 +38,12 @@ export interface ChipReadOptions {
|
|
|
37
38
|
successMessage?: string;
|
|
38
39
|
/** Read progress, so the UI can narrate an otherwise invisible operation. */
|
|
39
40
|
onStage?: (stage: NfcReadStage) => void;
|
|
41
|
+
/**
|
|
42
|
+
* The Active-Authentication challenge issued by the SERVER (8 bytes). Absent
|
|
43
|
+
* ⇒ the anti-clone step is skipped and the read is exactly what it was
|
|
44
|
+
* before — the whole capability null-degrades rather than failing.
|
|
45
|
+
*/
|
|
46
|
+
aaChallenge?: AaChallenge;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
/**
|
|
@@ -151,7 +158,7 @@ export async function readPassportChip(
|
|
|
151
158
|
|
|
152
159
|
try {
|
|
153
160
|
onStage('waiting');
|
|
154
|
-
const result = await readChip(native, mrz, onStage);
|
|
161
|
+
const result = await readChip(native, mrz, onStage, options.aaChallenge);
|
|
155
162
|
if (!result.sod && attempt < MAX_CHIP_ATTEMPTS && !abandoned()) {
|
|
156
163
|
partial = result;
|
|
157
164
|
devLog(attempt, 'security-file', new Error('EF.SOD missing — retrying for a complete read'));
|
package/src/emrtd/session.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { fromBase64, toBase64 } from './bytes';
|
|
2
2
|
import { buildBacChallenge, completeBac } from './bac';
|
|
3
3
|
import { primitivesFromNative, type EmrtdPrimitives, type MrzKeyFields } from './crypto';
|
|
4
|
+
import { readExtraGroups, type ExtraGroupReads } from './extras';
|
|
5
|
+
import { readActiveAuth, type AaChallenge, type ActiveAuthRead } from './activeAuth';
|
|
4
6
|
import { EF, readFile, type Transceive } from './files';
|
|
5
7
|
import { readOptionalFile } from './optionalRead';
|
|
6
8
|
import { PaceError, PREFER_PACE, tryPace, type PaceOutcome } from './open';
|
|
@@ -45,6 +47,22 @@ export interface EmrtdReadResult {
|
|
|
45
47
|
sod?: string;
|
|
46
48
|
/** DG2 — the portrait. Best-effort; the largest file and the likeliest to drop. */
|
|
47
49
|
dg2?: string;
|
|
50
|
+
/** DG7 — the displayed signature image. Best-effort, COM-gated (see extras.ts). */
|
|
51
|
+
dg7?: string;
|
|
52
|
+
/** DG11 — additional personal details. Best-effort; many issuers omit it. */
|
|
53
|
+
dg11?: string;
|
|
54
|
+
/** DG12 — additional document details. Best-effort; many issuers omit it. */
|
|
55
|
+
dg12?: string;
|
|
56
|
+
/** DG15 — the chip's Active-Authentication public key. Absent on the many
|
|
57
|
+
* chips that support no AA at all. */
|
|
58
|
+
dg15?: string;
|
|
59
|
+
/** The chip's signature over the SERVER's challenge — the anti-clone proof.
|
|
60
|
+
* Verified server-side only; a client that checked its own chip could be
|
|
61
|
+
* patched to say yes. */
|
|
62
|
+
aaSignature?: string;
|
|
63
|
+
/** Which server-issued challenge that signature answers — echoed back so the
|
|
64
|
+
* server can spend it. Not read off the chip; carried through with it. */
|
|
65
|
+
aaChallengeId?: string;
|
|
48
66
|
/** How the chip was unlocked. Reported to the server on the submission. */
|
|
49
67
|
chipAuth: 'bac' | 'pace';
|
|
50
68
|
/**
|
|
@@ -141,6 +159,12 @@ export async function readChip(
|
|
|
141
159
|
* enough that silence reads as failure and prompts them to lift the document.
|
|
142
160
|
*/
|
|
143
161
|
onStage?: (stage: NfcReadStage) => void,
|
|
162
|
+
/**
|
|
163
|
+
* The Active-Authentication challenge the SERVER issued, if it could. Ours to
|
|
164
|
+
* carry, never ours to choose: a client-chosen nonce makes a captured
|
|
165
|
+
* signature replayable, which is exactly the clone AA exists to catch.
|
|
166
|
+
*/
|
|
167
|
+
aaChallenge?: AaChallenge,
|
|
144
168
|
): Promise<EmrtdReadResult> {
|
|
145
169
|
const p = primitivesFromNative(native);
|
|
146
170
|
|
|
@@ -178,9 +202,20 @@ export async function readChip(
|
|
|
178
202
|
const sod = await readOptionalFile(sm, transceive, EF.SOD, 'EF.SOD');
|
|
179
203
|
|
|
180
204
|
let dg2: Uint8Array | null = null;
|
|
205
|
+
let extras: ExtraGroupReads = { dg7: null, dg11: null, dg12: null };
|
|
206
|
+
let activeAuth: ActiveAuthRead = {};
|
|
181
207
|
if (sod) {
|
|
182
208
|
onStage?.('readingPhoto');
|
|
183
209
|
dg2 = await readOptionalFile(sm, transceive, EF.DG2, 'DG2');
|
|
210
|
+
// The optional detail groups come LAST: by now everything that matters is
|
|
211
|
+
// banked, so a drop here costs only nice-to-have context. Gated on the SOD
|
|
212
|
+
// like DG2 — the server authenticates each group against it.
|
|
213
|
+
onStage?.('readingDetails');
|
|
214
|
+
extras = await readExtraGroups(sm, transceive);
|
|
215
|
+
// The anti-clone challenge LAST: it is the only step that asks the chip to
|
|
216
|
+
// compute rather than read, so it is the slowest per byte and the one worth
|
|
217
|
+
// losing if the document leaves contact.
|
|
218
|
+
activeAuth = await readActiveAuth(sm, transceive, aaChallenge);
|
|
184
219
|
}
|
|
185
220
|
onStage?.('done');
|
|
186
221
|
|
|
@@ -188,6 +223,12 @@ export async function readChip(
|
|
|
188
223
|
dg1: toBase64(dg1),
|
|
189
224
|
...(sod ? { sod: toBase64(sod) } : {}),
|
|
190
225
|
...(dg2 ? { dg2: toBase64(dg2) } : {}),
|
|
226
|
+
...(extras.dg7 ? { dg7: toBase64(extras.dg7) } : {}),
|
|
227
|
+
...(extras.dg11 ? { dg11: toBase64(extras.dg11) } : {}),
|
|
228
|
+
...(extras.dg12 ? { dg12: toBase64(extras.dg12) } : {}),
|
|
229
|
+
...(activeAuth.dg15 ? { dg15: activeAuth.dg15 } : {}),
|
|
230
|
+
...(activeAuth.signature ? { aaSignature: activeAuth.signature } : {}),
|
|
231
|
+
...(activeAuth.signature && aaChallenge ? { aaChallengeId: aaChallenge.id } : {}),
|
|
191
232
|
chipAuth: access.chipAuth,
|
|
192
233
|
paceOutcome: access.outcome,
|
|
193
234
|
...(access.detail ? { paceDetail: access.detail } : {}),
|
package/src/emrtd/stages.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type NfcReadStage =
|
|
|
23
23
|
| 'readingSecurity'
|
|
24
24
|
/** Reading DG2 — the chip's portrait. */
|
|
25
25
|
| 'readingPhoto'
|
|
26
|
+
/** Reading the optional detail groups (DG7/DG11/DG12) — small and quick. */
|
|
27
|
+
| 'readingDetails'
|
|
26
28
|
/** Everything that could be read has been. */
|
|
27
29
|
| 'done';
|
|
28
30
|
|
|
@@ -33,6 +35,7 @@ export const NFC_STAGE_ORDER: NfcReadStage[] = [
|
|
|
33
35
|
'readingData',
|
|
34
36
|
'readingSecurity',
|
|
35
37
|
'readingPhoto',
|
|
38
|
+
'readingDetails',
|
|
36
39
|
'done',
|
|
37
40
|
];
|
|
38
41
|
|
|
@@ -49,6 +52,8 @@ export function nfcStageLabel(stage: NfcReadStage): string {
|
|
|
49
52
|
return 'Reading security data';
|
|
50
53
|
case 'readingPhoto':
|
|
51
54
|
return 'Reading photo';
|
|
55
|
+
case 'readingDetails':
|
|
56
|
+
return 'Reading extra details';
|
|
52
57
|
case 'done':
|
|
53
58
|
return 'Chip read complete';
|
|
54
59
|
}
|
|
@@ -73,6 +78,8 @@ export function nfcStageDetail(stage: NfcReadStage): string {
|
|
|
73
78
|
return 'Downloading the chip’s digital signature. This is the largest part and takes the longest — keep holding.';
|
|
74
79
|
case 'readingPhoto':
|
|
75
80
|
return 'Copying the photo stored on the chip.';
|
|
81
|
+
case 'readingDetails':
|
|
82
|
+
return 'Copying the optional details stored on the chip. These are small and quick.';
|
|
76
83
|
case 'done':
|
|
77
84
|
return 'Everything was read successfully.';
|
|
78
85
|
}
|
|
@@ -98,6 +105,8 @@ export function nfcSheetMessage(stage: NfcReadStage): string {
|
|
|
98
105
|
return 'Verifying security data — keep holding…';
|
|
99
106
|
case 'readingPhoto':
|
|
100
107
|
return 'Reading your photo — keep holding…';
|
|
108
|
+
case 'readingDetails':
|
|
109
|
+
return 'Almost done — keep holding…';
|
|
101
110
|
case 'done':
|
|
102
111
|
return 'Chip read complete';
|
|
103
112
|
}
|
|
@@ -116,6 +125,8 @@ export function nfcStageProgress(stage: NfcReadStage): number {
|
|
|
116
125
|
return 0.6;
|
|
117
126
|
case 'readingPhoto':
|
|
118
127
|
return 0.85;
|
|
128
|
+
case 'readingDetails':
|
|
129
|
+
return 0.95;
|
|
119
130
|
case 'done':
|
|
120
131
|
return 1;
|
|
121
132
|
}
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
export { MyazaKYC, useMyazaKYC } from './MyazaKYC';
|
|
12
12
|
export type { MyazaKYCProps, UseMyazaKYCReturn } from './MyazaKYC';
|
|
13
13
|
|
|
14
|
+
// Returning-user face re-authentication (the web SDK's MyazaBiometricAuth).
|
|
15
|
+
export { MyazaBiometricAuth } from './MyazaBiometricAuth';
|
|
16
|
+
export type { MyazaBiometricAuthProps } from './MyazaBiometricAuth';
|
|
17
|
+
export type {
|
|
18
|
+
BiometricAuthRequest,
|
|
19
|
+
BiometricAuthResponse,
|
|
20
|
+
BiometricStatusResponse,
|
|
21
|
+
BiometricLivenessClaim,
|
|
22
|
+
} from './services/api-types-biometric';
|
|
23
|
+
|
|
14
24
|
// Public config + callback types
|
|
15
25
|
export type {
|
|
16
26
|
MyazaKYCConfig,
|
|
@@ -32,6 +42,7 @@ export type {
|
|
|
32
42
|
PhoneVerificationConfig,
|
|
33
43
|
ProofOfAddressConfig,
|
|
34
44
|
PoaDocumentType,
|
|
45
|
+
AddressCollectionConfig,
|
|
35
46
|
QuestionnaireConfig,
|
|
36
47
|
QuestionnaireField,
|
|
37
48
|
QuestionnaireFieldOption,
|
|
@@ -56,7 +67,8 @@ export type {
|
|
|
56
67
|
WorkflowBusinessDocumentTypeConfig,
|
|
57
68
|
WorkflowBusinessApplicantConfig,
|
|
58
69
|
} from './types/business';
|
|
59
|
-
export type { KYCSubmission, KYCErrorCode, KYCErrorDetails } from './types/verification';
|
|
70
|
+
export type { KYCSubmission, KYCResult, KYCErrorCode, KYCErrorDetails } from './types/verification';
|
|
71
|
+
export type { BiometricFlowConfig, BiometricCopy, BiometricCopyText } from './config/biometricOptions';
|
|
60
72
|
export { KYCError } from './types/verification';
|
|
61
73
|
|
|
62
74
|
// ID-type matrix + helpers
|
|
@@ -107,6 +119,47 @@ export {
|
|
|
107
119
|
export { hasActiveQuestionnaire, questionnaireAnswerKeys } from './config/questionnaire';
|
|
108
120
|
export { hasEmailVerificationStep, hasPhoneVerificationStep } from './config/contact';
|
|
109
121
|
export { hasProofOfAddressStep, poaDocumentTypes, poaTypeLabel } from './config/proofOfAddress';
|
|
122
|
+
export { hasAddressCollectionStep } from './config/addressCollection';
|
|
123
|
+
// Presence verification (Address Intelligence Phase 2): the foreground
|
|
124
|
+
// reporter the HOST APP calls on app open, plus the on-device pin store.
|
|
125
|
+
export {
|
|
126
|
+
reportAddressPresence,
|
|
127
|
+
type ReportPresenceOptions,
|
|
128
|
+
type ReportPresenceResult,
|
|
129
|
+
} from './presence/report';
|
|
130
|
+
// The background (OS geofence) tier — the always-on model. register* must run
|
|
131
|
+
// at app-root module scope; enable* asks for the background permission.
|
|
132
|
+
export {
|
|
133
|
+
PRESENCE_GEOFENCE_TASK,
|
|
134
|
+
disableBackgroundPresence,
|
|
135
|
+
enableBackgroundPresence,
|
|
136
|
+
registerBackgroundPresence,
|
|
137
|
+
type EnableBackgroundResult,
|
|
138
|
+
} from './presence/background';
|
|
139
|
+
// The Android foreground-service tier (the OkHi reliability move): a
|
|
140
|
+
// persistent notification keeps the process alive on phones whose battery
|
|
141
|
+
// managers drop geofence transitions. Opt-in; the host words the notification.
|
|
142
|
+
export {
|
|
143
|
+
PRESENCE_LOCATION_TASK,
|
|
144
|
+
disableForegroundService,
|
|
145
|
+
enableForegroundService,
|
|
146
|
+
foregroundServiceRunning,
|
|
147
|
+
type EnableForegroundServiceResult,
|
|
148
|
+
type PresenceNotification,
|
|
149
|
+
} from './presence/foreground-service';
|
|
150
|
+
export {
|
|
151
|
+
openLocationSettings,
|
|
152
|
+
presenceStatus,
|
|
153
|
+
type PresenceStatus,
|
|
154
|
+
type PresenceTier,
|
|
155
|
+
} from './presence/status';
|
|
156
|
+
export { resolvePresenceTier, type PermissionState, type TierInputs } from './presence/tier';
|
|
157
|
+
// savePresencePin is the org-side handoff for STANDALONE address verification:
|
|
158
|
+
// when capture happened on a hosted web link (or the org's own backend already
|
|
159
|
+
// holds the address), the host app hands the SDK the pin so the foreground and
|
|
160
|
+
// background presence tiers can run — the pin otherwise only ever exists where
|
|
161
|
+
// OUR capture step stored it.
|
|
162
|
+
export { clearPresencePin, savePresencePin } from './presence/store';
|
|
110
163
|
|
|
111
164
|
// Country grouping for multi-region flows
|
|
112
165
|
export { groupCountriesByRegion, regionCountryName, type Region } from './config/regions';
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { AddressParts } from '../services/api';
|
|
2
|
+
import type { LocationFailure } from '../services/location';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// The device's current location, fetched ONCE per verification and shared by
|
|
6
|
+
// every address step.
|
|
7
|
+
//
|
|
8
|
+
// Module-level on purpose: the steps mount and unmount as the person walks the
|
|
9
|
+
// flow, and re-prompting or re-fixing on every screen is exactly the hesitation
|
|
10
|
+
// this exists to remove. Every address step starts the prefetch on mount, so
|
|
11
|
+
// the GPS warms up while the person is still reading the SEARCH screen; by the
|
|
12
|
+
// pin step the fix — and its reverse-geocoded line — are usually already in
|
|
13
|
+
// hand, and the map lands right first time instead of showing a default view
|
|
14
|
+
// and then jumping.
|
|
15
|
+
//
|
|
16
|
+
// A MIRROR of the web SDK's steps/address/current-location.ts, with one
|
|
17
|
+
// deliberate difference: web scopes the cache to a PAGE session, which ends at
|
|
18
|
+
// the next reload. An app session spans many opens of the SDK, so a fix cached
|
|
19
|
+
// an hour ago would drop the pin at yesterday's address. Store CREATION clears
|
|
20
|
+
// it (one store per modal launch), making "this verification" the mobile
|
|
21
|
+
// equivalent of that scope. Clearing it only from the store's reset() is not
|
|
22
|
+
// enough: reset() is a consumer-facing restart, not part of the open path.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export interface CurrentFix {
|
|
26
|
+
lat: number;
|
|
27
|
+
lng: number;
|
|
28
|
+
accuracy: number | null;
|
|
29
|
+
/** The reverse-geocoded line ("11 Bassey Street, Calabar"), when known. */
|
|
30
|
+
label: string | null;
|
|
31
|
+
parts: AddressParts | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A GPS read: the fix, or WHY there is none (services/location.ts). */
|
|
35
|
+
export type FixOutcome =
|
|
36
|
+
| { fix: { lat: number; lng: number; accuracy: number | null } }
|
|
37
|
+
| { failure: LocationFailure };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What the cache needs to produce a fix.
|
|
41
|
+
*
|
|
42
|
+
* The GPS reader is INJECTED rather than imported: the store's reset clears
|
|
43
|
+
* this cache, and importing expo-location here would drag a native module into
|
|
44
|
+
* the store's graph for the sake of resetting three variables.
|
|
45
|
+
*/
|
|
46
|
+
export interface FixSource {
|
|
47
|
+
takeFix(): Promise<FixOutcome>;
|
|
48
|
+
addressReverse(
|
|
49
|
+
lat: number,
|
|
50
|
+
lng: number,
|
|
51
|
+
): Promise<{ line: string | null; parts?: AddressParts | null }>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let resolved: CurrentFix | null = null;
|
|
55
|
+
let inflight: Promise<CurrentFix | null> | null = null;
|
|
56
|
+
let failed = false;
|
|
57
|
+
let lastFailure: LocationFailure | null = null;
|
|
58
|
+
|
|
59
|
+
/** Why the last attempt failed, so the message can say something true. */
|
|
60
|
+
export function currentFixFailure(): LocationFailure | null {
|
|
61
|
+
return lastFailure;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The message for a failed read. A refused permission, a phone that cannot
|
|
66
|
+
* place itself, and a fix that took too long are three different problems
|
|
67
|
+
* with three different remedies, and one line telling everybody to "allow
|
|
68
|
+
* location access" sent people to a permission that was already granted.
|
|
69
|
+
* Placing the pin by hand always works, so every line says so. Mirrors the
|
|
70
|
+
* web SDK's locationFailureMessage and Flutter's.
|
|
71
|
+
*/
|
|
72
|
+
export function locationFailureMessage(reason: LocationFailure | null): string {
|
|
73
|
+
switch (reason) {
|
|
74
|
+
case 'denied':
|
|
75
|
+
return "Location access is blocked for this app. Allow it in your phone's Settings, then try again, or place the pin yourself.";
|
|
76
|
+
case 'unavailable':
|
|
77
|
+
return 'Your phone could not work out where it is right now. Check that location is switched on, then try again, or place the pin yourself.';
|
|
78
|
+
case 'timeout':
|
|
79
|
+
return 'Finding your location took too long. Try again, or place the pin yourself.';
|
|
80
|
+
default:
|
|
81
|
+
return 'This device cannot share its location. Place the pin yourself.';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The fix, when one has already resolved for this verification. */
|
|
86
|
+
export function currentFix(): CurrentFix | null {
|
|
87
|
+
return resolved;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Is a fix attempt still running? */
|
|
91
|
+
export function locating(): boolean {
|
|
92
|
+
return inflight !== null && resolved === null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Start (or JOIN) the one location attempt. Safe to call from every address
|
|
97
|
+
* step's mount: the OS permission prompt fires at most once, and a second
|
|
98
|
+
* caller waits on the first attempt rather than starting its own.
|
|
99
|
+
*
|
|
100
|
+
* Resolves null on a denied or failed read — callers fall back to the manual
|
|
101
|
+
* pin, which is what every address failure degrades to.
|
|
102
|
+
*/
|
|
103
|
+
export function prefetchCurrentFix(
|
|
104
|
+
source: FixSource,
|
|
105
|
+
opts?: { retry?: boolean },
|
|
106
|
+
): Promise<CurrentFix | null> {
|
|
107
|
+
if (resolved) return Promise.resolve(resolved);
|
|
108
|
+
// A failed attempt (denied prompt, no fix) is never retried AUTOMATICALLY.
|
|
109
|
+
// Every step mount calls this, so re-arming on failure left the location row
|
|
110
|
+
// spinning forever and re-fired the OS prompt on each screen. An explicit
|
|
111
|
+
// TAP passes `retry` and gets a fresh attempt, since the person may have
|
|
112
|
+
// granted permission in the meantime.
|
|
113
|
+
if (failed && !inflight && !opts?.retry) return Promise.resolve(null);
|
|
114
|
+
if (!inflight) {
|
|
115
|
+
inflight = (async () => {
|
|
116
|
+
// No builder-preview branch: the preview is a web-only surface, and a
|
|
117
|
+
// canned fix here would be a code path no mobile caller can reach.
|
|
118
|
+
const outcome = await source.takeFix().catch((): FixOutcome => ({ failure: 'unsupported' }));
|
|
119
|
+
if (!('fix' in outcome)) {
|
|
120
|
+
inflight = null;
|
|
121
|
+
failed = true;
|
|
122
|
+
lastFailure = outcome.failure;
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const fix = outcome.fix;
|
|
126
|
+
lastFailure = null;
|
|
127
|
+
let label: string | null = null;
|
|
128
|
+
let parts: AddressParts | null = null;
|
|
129
|
+
try {
|
|
130
|
+
const r = await source.addressReverse(fix.lat, fix.lng);
|
|
131
|
+
label = r.line ?? null;
|
|
132
|
+
parts = r.parts ?? null;
|
|
133
|
+
} catch {
|
|
134
|
+
/* the coordinates alone are still a fix */
|
|
135
|
+
}
|
|
136
|
+
resolved = { lat: fix.lat, lng: fix.lng, accuracy: fix.accuracy, label, parts };
|
|
137
|
+
failed = false;
|
|
138
|
+
return resolved;
|
|
139
|
+
})();
|
|
140
|
+
}
|
|
141
|
+
return inflight;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Forget the fix so the next verification takes its own. Called from the
|
|
145
|
+
* store's reset, beside the step log. */
|
|
146
|
+
export function resetCurrentFix(): void {
|
|
147
|
+
resolved = null;
|
|
148
|
+
inflight = null;
|
|
149
|
+
failed = false;
|
|
150
|
+
lastFailure = null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── The pin step's automatic first locate ───────────────────────────────────
|
|
154
|
+
//
|
|
155
|
+
// Most people are verifying from home, so the map should land on them rather
|
|
156
|
+
// than a country-centre default. It fires ONCE: a dismissed or denied prompt
|
|
157
|
+
// must not re-fire every time the person passes back through the pin step, and
|
|
158
|
+
// the explicit control is there for retries.
|
|
159
|
+
|
|
160
|
+
let autoLocateAttempted = false;
|
|
161
|
+
|
|
162
|
+
/** True the FIRST time it is called for a verification, false after. */
|
|
163
|
+
export function claimAutoLocate(): boolean {
|
|
164
|
+
if (autoLocateAttempted) return false;
|
|
165
|
+
autoLocateAttempted = true;
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function resetAutoLocate(): void {
|
|
170
|
+
autoLocateAttempted = false;
|
|
171
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { AddressCollectionConfig } from '../types/workflow';
|
|
2
|
+
import type { AddressState } from '../store/state';
|
|
3
|
+
|
|
4
|
+
// Per-field modes for the typed details-sheet fields — the CLIENT MIRROR of
|
|
5
|
+
// the server's lib/workflows/address-fields.ts (kyc-core) and of the web
|
|
6
|
+
// SDK's steps/address/address-field-modes.ts. Keep the resolution rule in
|
|
7
|
+
// lockstep: the legacy propertyFields group switch is the default for every
|
|
8
|
+
// typed field, propertyFields 'required' requires the house/flat NUMBER, and
|
|
9
|
+
// fields.<key> overrides per key. The three mirrors share one vector file
|
|
10
|
+
// (kyc-sdk-flutter/test/address_field_modes_vectors.json).
|
|
11
|
+
//
|
|
12
|
+
// Why it matters here: the server 422s a submission whose required fields
|
|
13
|
+
// never arrived. Without this mirror a mobile applicant walked the whole
|
|
14
|
+
// flow, and the refusal landed on the last screen with nothing to act on.
|
|
15
|
+
|
|
16
|
+
export const ADDRESS_FIELD_KEYS = [
|
|
17
|
+
'propertyName',
|
|
18
|
+
'propertyNumber',
|
|
19
|
+
'street',
|
|
20
|
+
'unit',
|
|
21
|
+
'neighbourhood',
|
|
22
|
+
'city',
|
|
23
|
+
'state',
|
|
24
|
+
'postcode',
|
|
25
|
+
] as const;
|
|
26
|
+
export type AddressFieldKey = (typeof ADDRESS_FIELD_KEYS)[number];
|
|
27
|
+
export type AddressFieldMode = 'off' | 'optional' | 'required';
|
|
28
|
+
|
|
29
|
+
export const ADDRESS_FIELD_LABELS: Record<AddressFieldKey, string> = {
|
|
30
|
+
propertyName: 'Building name',
|
|
31
|
+
propertyNumber: 'House or flat number',
|
|
32
|
+
street: 'Street name',
|
|
33
|
+
unit: 'Unit',
|
|
34
|
+
neighbourhood: 'Neighbourhood',
|
|
35
|
+
city: 'City',
|
|
36
|
+
state: 'State',
|
|
37
|
+
postcode: 'Area code',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const MODES: readonly string[] = ['off', 'optional', 'required'];
|
|
41
|
+
function isAddressFieldMode(value: unknown): value is AddressFieldMode {
|
|
42
|
+
return typeof value === 'string' && MODES.includes(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function addressFieldModes(
|
|
46
|
+
config: AddressCollectionConfig | undefined | null,
|
|
47
|
+
): Record<AddressFieldKey, AddressFieldMode> {
|
|
48
|
+
const group = config?.propertyFields ?? 'optional';
|
|
49
|
+
const modes = {} as Record<AddressFieldKey, AddressFieldMode>;
|
|
50
|
+
for (const key of ADDRESS_FIELD_KEYS) {
|
|
51
|
+
const fallback: AddressFieldMode =
|
|
52
|
+
group === 'off' ? 'off' : group === 'required' && key === 'propertyNumber' ? 'required' : 'optional';
|
|
53
|
+
// An override outside the vocabulary falls back rather than being taken
|
|
54
|
+
// literally: the server validates writes, the client validates reads.
|
|
55
|
+
const override = config?.fields?.[key];
|
|
56
|
+
modes[key] = isAddressFieldMode(override) ? override : fallback;
|
|
57
|
+
}
|
|
58
|
+
return modes;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Which map-prefill part fills each field while untouched (the sheet's own
|
|
62
|
+
* `shown` rule). Property name/number and unit have no prefill — the
|
|
63
|
+
* applicant alone can know them. */
|
|
64
|
+
const PART_OF: Partial<Record<AddressFieldKey, 'street' | 'area' | 'city' | 'state' | 'postcode'>> = {
|
|
65
|
+
street: 'street',
|
|
66
|
+
neighbourhood: 'area',
|
|
67
|
+
city: 'city',
|
|
68
|
+
state: 'state',
|
|
69
|
+
postcode: 'postcode',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** The value the sheet DISPLAYS for a field: typed wins (a cleared field
|
|
73
|
+
* stays cleared), else the map's prefill. */
|
|
74
|
+
export function displayedAddressValue(key: AddressFieldKey, address: AddressState): string {
|
|
75
|
+
const typed = (address[key] as string | undefined)?.trim();
|
|
76
|
+
if (typed !== undefined) return typed;
|
|
77
|
+
const part = PART_OF[key];
|
|
78
|
+
return (part ? address.parts?.[part] : null)?.trim() ?? '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Required fields whose DISPLAYED value is blank — what holds Continue. */
|
|
82
|
+
export function missingRequiredAddressFields(
|
|
83
|
+
config: AddressCollectionConfig | undefined | null,
|
|
84
|
+
address: AddressState | null | undefined,
|
|
85
|
+
): AddressFieldKey[] {
|
|
86
|
+
if (!address) return [];
|
|
87
|
+
const modes = addressFieldModes(config);
|
|
88
|
+
return ADDRESS_FIELD_KEYS.filter(
|
|
89
|
+
(key) => modes[key] === 'required' && displayedAddressValue(key, address) === '',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The nudge under Continue when required fields are still blank. */
|
|
94
|
+
export function missingFieldsNudge(missing: AddressFieldKey[]): string {
|
|
95
|
+
return `This flow needs: ${missing.map((k) => ADDRESS_FIELD_LABELS[k].toLowerCase()).join(', ')}.`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The map-prefill values a REQUIRED field rides to the server when the
|
|
100
|
+
* applicant left it untouched: they saw it filled and confirmed it by
|
|
101
|
+
* continuing, so the wire must carry it — the server 422s a required field
|
|
102
|
+
* that never arrives. Typed values are absent here on purpose (the normal
|
|
103
|
+
* assembly already sends them), so spreading this LAST overrides nothing.
|
|
104
|
+
*/
|
|
105
|
+
export function requiredPrefillSubmission(
|
|
106
|
+
config: AddressCollectionConfig | undefined | null,
|
|
107
|
+
address: AddressState,
|
|
108
|
+
): Partial<Record<AddressFieldKey, string>> {
|
|
109
|
+
const modes = addressFieldModes(config);
|
|
110
|
+
const out: Partial<Record<AddressFieldKey, string>> = {};
|
|
111
|
+
for (const key of ADDRESS_FIELD_KEYS) {
|
|
112
|
+
if (modes[key] !== 'required') continue;
|
|
113
|
+
const typed = (address[key] as string | undefined)?.trim();
|
|
114
|
+
if (typed) continue;
|
|
115
|
+
const displayed = displayedAddressValue(key, address);
|
|
116
|
+
if (displayed) out[key] = displayed;
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|