@myazahq/kyc-sdk-react-native 2.5.0 → 3.0.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 +277 -7
- package/android/build.gradle +30 -16
- package/android/consumer-rules.pro +22 -0
- package/android/src/main/AndroidManifest.xml +17 -6
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +27 -70
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +14 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/MlKitModelReadiness.kt +154 -0
- package/app.plugin.js +146 -10
- package/ios/HybridMyazaFaceDetector.swift +8 -3
- package/ios/HybridMyazaTextRecognizer.swift +8 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +21 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +8 -0
- package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +16 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +31 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +2 -0
- package/package.json +30 -9
- package/src/MyazaKYC.tsx +4 -11
- package/src/assets/fonts/Karla_400Regular.ttf +0 -0
- package/src/assets/fonts/Karla_500Medium.ttf +0 -0
- package/src/assets/fonts/Karla_600SemiBold.ttf +0 -0
- package/src/assets/fonts/Karla_700Bold.ttf +0 -0
- package/src/assets/fonts/OFL-Karla.txt +93 -0
- package/src/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
- package/src/assets/fonts/README.md +12 -0
- package/src/assets/fonts/SpaceGrotesk_500Medium.ttf +0 -0
- package/src/assets/fonts/SpaceGrotesk_600SemiBold.ttf +0 -0
- package/src/assets/fonts/SpaceGrotesk_700Bold.ttf +0 -0
- package/src/capture/useAutoCapture.ts +8 -1
- package/src/components/CountryField.tsx +8 -0
- package/src/components/CountryRegionPicker.tsx +184 -0
- package/src/components/DialCodePicker.tsx +47 -68
- package/src/components/DialCodeRow.tsx +102 -0
- package/src/components/DocumentCropper.tsx +30 -24
- package/src/components/DocumentReview.tsx +11 -1
- package/src/components/DocumentReviewSide.tsx +6 -2
- package/src/components/DocumentReviewZoom.tsx +5 -1
- package/src/components/FramedMapPicker.tsx +122 -0
- package/src/components/GeoBadge.tsx +34 -0
- package/src/components/Icon.tsx +10 -192
- package/src/components/KycFlow.tsx +69 -34
- 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/MyazaButton.tsx +9 -0
- package/src/components/MyazaInput.tsx +17 -1
- package/src/components/OptionRow.tsx +22 -3
- package/src/components/PhoneNumberInput.tsx +8 -0
- package/src/components/PoweredBy.tsx +5 -5
- package/src/components/PresenceBlocks.tsx +153 -0
- package/src/components/RequiredDocumentPill.tsx +114 -0
- package/src/components/StepHeader.tsx +23 -3
- package/src/components/StepView.tsx +17 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/documentReviewCopy.ts +39 -0
- package/src/components/fonts.ts +14 -17
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +48 -5
- package/src/config/addressCollection.ts +125 -0
- package/src/config/biometricOptions.ts +100 -0
- package/src/config/business.ts +20 -1
- package/src/config/businessSteps.ts +8 -2
- package/src/config/consentStep.ts +19 -0
- package/src/config/documentCaptureMethods.ts +31 -0
- package/src/config/proofOfAddress.ts +54 -11
- package/src/config/regions.ts +24 -0
- package/src/config/stepOrder.ts +79 -5
- package/src/config/uploadLimits.ts +35 -0
- package/src/config/workflowMerge.ts +13 -5
- package/src/index.ts +49 -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-copy.ts +51 -0
- package/src/lib/captureRing.ts +83 -0
- package/src/lib/country-adoption.ts +89 -0
- package/src/lib/documentCaptureCheck.ts +136 -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/model-ready.ts +82 -0
- package/src/lib/poa-country-gate.ts +27 -0
- package/src/lib/prime-models.ts +50 -0
- package/src/lib/resubmit.ts +36 -7
- 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/selfie-sharpness.ts +211 -0
- package/src/lib/selfie-upload-wait.ts +79 -0
- package/src/lib/street-view-fov.ts +42 -0
- package/src/lib/webview-available.ts +47 -0
- package/src/liveness/avatarSource.ts +59 -0
- package/src/liveness/useLiveness.ts +6 -1
- package/src/liveness/useModelReady.ts +9 -55
- package/src/mrz/extract.ts +71 -4
- package/src/mrz/textRecognizer.ts +36 -0
- package/src/mrz/useTextModelReady.ts +21 -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/BusinessDetailsFields.tsx +3 -0
- package/src/screens/BusinessDetailsStep.tsx +2 -0
- package/src/screens/BusinessDocumentSlot.tsx +3 -2
- package/src/screens/BusinessDocumentsStep.tsx +4 -55
- package/src/screens/CompanyInfoFields.tsx +6 -1
- package/src/screens/ConsentStep.tsx +6 -17
- package/src/screens/ContactActions.tsx +54 -0
- package/src/screens/ContactDestinationField.tsx +4 -0
- package/src/screens/ContactEntryPanel.tsx +75 -0
- package/src/screens/ContactVerificationStep.tsx +26 -56
- package/src/screens/CountrySelectStep.tsx +13 -147
- package/src/screens/DocumentCaptureStep.tsx +177 -93
- package/src/screens/LivenessAvatar.tsx +33 -16
- package/src/screens/LivenessStep.tsx +188 -49
- package/src/screens/MrzScanView.tsx +33 -0
- package/src/screens/PoaDocumentTypeList.tsx +66 -0
- package/src/screens/ProofOfAddressParts.tsx +151 -0
- package/src/screens/ProofOfAddressStep.tsx +58 -99
- 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 +71 -164
- 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/consent/model.ts +79 -10
- package/src/screens/document/CaptureCheckNotice.tsx +77 -0
- package/src/screens/document/UploadPhase.tsx +177 -0
- package/src/screens/document/useDocumentCamera.ts +109 -0
- 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 +8 -4
- package/src/screens/liveness/SelfiePreview.tsx +31 -7
- 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/NfcScanIllustration.tsx +7 -18
- package/src/screens/useAddressPhotoAttach.ts +77 -0
- package/src/screens/useBusinessDocumentAttach.ts +93 -0
- package/src/screens/usePoaAttach.ts +12 -15
- package/src/services/api-types.ts +119 -1
- package/src/services/api-verify-types.ts +37 -0
- package/src/services/api.ts +149 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/location.ts +234 -0
- package/src/services/mediaCompress.ts +23 -5
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +7 -0
- package/src/specs/MyazaTextRecognizer.nitro.ts +29 -0
- package/src/store/address.ts +97 -0
- package/src/store/derive.ts +29 -4
- package/src/store/kycStore.ts +120 -5
- package/src/store/serverConfig.ts +18 -0
- package/src/store/session.ts +23 -4
- package/src/store/state.ts +134 -0
- package/src/store/submit.ts +18 -1
- package/src/types/config.ts +63 -1
- package/src/types/verification.ts +12 -0
- package/src/types/workflow.ts +97 -1
- package/src/assets/liveness/Blink.gif +0 -0
- package/src/assets/liveness/Nod.gif +0 -0
- package/src/assets/liveness/Smile.gif +0 -0
- package/src/assets/liveness/Turn.gif +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Web Mercator math for the dependency-free map pin picker. Pure and
|
|
2
|
+
// unit-tested — the picker is a thin gesture shell over these.
|
|
3
|
+
//
|
|
4
|
+
// Deliberately NOT react-native-maps: that means native modules, per-platform
|
|
5
|
+
// setup and a Google Maps API key per org for what the picker needs — pan,
|
|
6
|
+
// zoom, one pin. The standard slippy-map maths below is ~60 lines, fully
|
|
7
|
+
// testable, and the tiles are plain <Image>s.
|
|
8
|
+
//
|
|
9
|
+
// A MIRROR of the web SDK's lib/map-tiles.ts — keep the two in lockstep.
|
|
10
|
+
|
|
11
|
+
export const TILE_SIZE = 256;
|
|
12
|
+
export const MIN_ZOOM = 3;
|
|
13
|
+
export const MAX_ZOOM = 19;
|
|
14
|
+
|
|
15
|
+
// Web Mercator's poles — beyond this the projection diverges.
|
|
16
|
+
const MAX_LAT = 85.05112878;
|
|
17
|
+
|
|
18
|
+
export interface LatLng {
|
|
19
|
+
lat: number;
|
|
20
|
+
lng: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function clampLat(lat: number): number {
|
|
24
|
+
return Math.max(-MAX_LAT, Math.min(MAX_LAT, lat));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function wrapLng(lng: number): number {
|
|
28
|
+
let l = lng;
|
|
29
|
+
while (l > 180) l -= 360;
|
|
30
|
+
while (l < -180) l += 360;
|
|
31
|
+
return l;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** World size in pixels at a zoom level. */
|
|
35
|
+
export function worldSize(zoom: number): number {
|
|
36
|
+
return TILE_SIZE * 2 ** zoom;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Project a coordinate to world pixels at a zoom level. */
|
|
40
|
+
export function latLngToWorld(point: LatLng, zoom: number): { x: number; y: number } {
|
|
41
|
+
const size = worldSize(zoom);
|
|
42
|
+
const lat = clampLat(point.lat);
|
|
43
|
+
const sin = Math.sin((lat * Math.PI) / 180);
|
|
44
|
+
// Clamped into the world: at the pole cap the log term lands a float
|
|
45
|
+
// epsilon outside [0, size], which would render as a phantom tile row.
|
|
46
|
+
const y = (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * size;
|
|
47
|
+
return {
|
|
48
|
+
x: ((wrapLng(point.lng) + 180) / 360) * size,
|
|
49
|
+
y: Math.min(size, Math.max(0, y)),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Unproject world pixels back to a coordinate. */
|
|
54
|
+
export function worldToLatLng(x: number, y: number, zoom: number): LatLng {
|
|
55
|
+
const size = worldSize(zoom);
|
|
56
|
+
const n = Math.PI - (2 * Math.PI * y) / size;
|
|
57
|
+
return {
|
|
58
|
+
lat: (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))),
|
|
59
|
+
lng: wrapLng((x / size) * 360 - 180),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface TilePlacement {
|
|
64
|
+
key: string;
|
|
65
|
+
url: string;
|
|
66
|
+
/** CSS position relative to the viewport's top-left corner. */
|
|
67
|
+
left: number;
|
|
68
|
+
top: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The OSM tiles covering a viewport centred on `center`. Tiles outside the
|
|
73
|
+
* world (above/below the poles) are skipped; longitude wraps.
|
|
74
|
+
*/
|
|
75
|
+
export function visibleTiles(
|
|
76
|
+
center: LatLng,
|
|
77
|
+
zoom: number,
|
|
78
|
+
width: number,
|
|
79
|
+
height: number,
|
|
80
|
+
): TilePlacement[] {
|
|
81
|
+
const world = latLngToWorld(center, zoom);
|
|
82
|
+
const tiles: TilePlacement[] = [];
|
|
83
|
+
const tileCount = 2 ** zoom;
|
|
84
|
+
const originX = world.x - width / 2;
|
|
85
|
+
const originY = world.y - height / 2;
|
|
86
|
+
const first = { x: Math.floor(originX / TILE_SIZE), y: Math.floor(originY / TILE_SIZE) };
|
|
87
|
+
const last = {
|
|
88
|
+
x: Math.floor((originX + width) / TILE_SIZE),
|
|
89
|
+
y: Math.floor((originY + height) / TILE_SIZE),
|
|
90
|
+
};
|
|
91
|
+
for (let ty = first.y; ty <= last.y; ty += 1) {
|
|
92
|
+
if (ty < 0 || ty >= tileCount) continue;
|
|
93
|
+
for (let tx = first.x; tx <= last.x; tx += 1) {
|
|
94
|
+
const wrappedX = ((tx % tileCount) + tileCount) % tileCount;
|
|
95
|
+
tiles.push({
|
|
96
|
+
key: `${zoom}/${tx}/${ty}`,
|
|
97
|
+
url: `https://tile.openstreetmap.org/${zoom}/${wrappedX}/${ty}.png`,
|
|
98
|
+
left: tx * TILE_SIZE - originX,
|
|
99
|
+
top: ty * TILE_SIZE - originY,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return tiles;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The centre after a drag of (dx, dy) viewport pixels. */
|
|
107
|
+
export function panCenter(center: LatLng, zoom: number, dx: number, dy: number): LatLng {
|
|
108
|
+
const world = latLngToWorld(center, zoom);
|
|
109
|
+
const next = worldToLatLng(world.x - dx, world.y - dy, zoom);
|
|
110
|
+
return { lat: clampLat(next.lat), lng: next.lng };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Sensible starting views: gov-DB countries at country zoom, else a world view. */
|
|
114
|
+
const COUNTRY_CENTERS: Record<string, LatLng> = {
|
|
115
|
+
NG: { lat: 9.06, lng: 8.68 },
|
|
116
|
+
GH: { lat: 7.95, lng: -1.02 },
|
|
117
|
+
KE: { lat: 0.02, lng: 37.9 },
|
|
118
|
+
ZA: { lat: -28.48, lng: 24.68 },
|
|
119
|
+
CI: { lat: 7.54, lng: -5.55 },
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export function defaultMapView(country: string | undefined): { center: LatLng; zoom: number } {
|
|
123
|
+
const center = country ? COUNTRY_CENTERS[country.toUpperCase()] : undefined;
|
|
124
|
+
return center ? { center, zoom: 6 } : { center: { lat: 6.5, lng: 12 }, zoom: 3 };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The height of the map surface on the pin step, from the viewport.
|
|
129
|
+
*
|
|
130
|
+
* A phone gets 40% of its height, clamped to 240–360: on a phone the map
|
|
131
|
+
* owns every touch, so the taller it is the less page is left to scroll on,
|
|
132
|
+
* and Continue has to stay reachable. From a tablet-class width (the web
|
|
133
|
+
* SDK's `sm` breakpoint) it is a flat 420. Mirrors the web SDK's
|
|
134
|
+
* `h-[40vh] min-h-[240px] max-h-[360px] sm:h-[420px]` on AddressCollectionStep
|
|
135
|
+
* and the Flutter `mapSurfaceHeight`; keep the three in lockstep.
|
|
136
|
+
*/
|
|
137
|
+
export const MAP_SURFACE = { phoneFraction: 0.4, phoneMin: 240, phoneMax: 360, wideBreakpoint: 640, wide: 420 } as const;
|
|
138
|
+
|
|
139
|
+
export function mapSurfaceHeight(viewport: { width: number; height: number }): number {
|
|
140
|
+
if (viewport.width >= MAP_SURFACE.wideBreakpoint) return MAP_SURFACE.wide;
|
|
141
|
+
const fraction = Math.round(viewport.height * MAP_SURFACE.phoneFraction);
|
|
142
|
+
return Math.min(MAP_SURFACE.phoneMax, Math.max(MAP_SURFACE.phoneMin, fraction));
|
|
143
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
// ─── The on-device model readiness gate ───────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Android fetches ML Kit's models through Play Services rather than bundling
|
|
6
|
+
// them, which keeps ~18.5 MB per device out of the APK but leaves a window
|
|
7
|
+
// where a detector cannot run: first launch before the download lands, or a
|
|
8
|
+
// device with no Google Play Services at all.
|
|
9
|
+
//
|
|
10
|
+
// This has to be answered BEFORE the camera opens, and neither detector can
|
|
11
|
+
// answer it itself. The face detector reports through `FaceResult`, where a
|
|
12
|
+
// missing model and an empty frame are both `faceCount: 0`; the text recogniser
|
|
13
|
+
// reports through `TextResult`, where both are an empty `lines` array. Gating on
|
|
14
|
+
// the per-frame result would strand the user on "position your face", or aiming
|
|
15
|
+
// at a passport that never reads, with the SDK unable to say why. That single
|
|
16
|
+
// failure mode is the whole reason this gate exists.
|
|
17
|
+
//
|
|
18
|
+
// Shared by both because the rule and the reasoning are identical — a copy per
|
|
19
|
+
// detector would be two places for one decision to drift.
|
|
20
|
+
//
|
|
21
|
+
// iOS is always ready (Apple Vision is a system framework), so this resolves on
|
|
22
|
+
// the first tick there and costs nothing.
|
|
23
|
+
|
|
24
|
+
export type ModelReadyState = 'ready' | 'preparing' | 'unavailable';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How long to wait for a model before calling it unavailable. Generous on
|
|
28
|
+
* purpose: the text model is about 10 MB, the native side requests it as an
|
|
29
|
+
* urgent install, and a slow connection is the common case. The cost is that a
|
|
30
|
+
* phone with no Play Services at all waits the full minute before being told;
|
|
31
|
+
* the bundled build (`myazaKycBundledMlKit`) is the answer for those fleets.
|
|
32
|
+
*/
|
|
33
|
+
const MODEL_WAIT_MS = 60_000;
|
|
34
|
+
/** Gap between readiness polls while the download is in flight. */
|
|
35
|
+
const POLL_INTERVAL_MS = 500;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Tracks whether an on-device model can run.
|
|
39
|
+
*
|
|
40
|
+
* Returns `'preparing'` while Play Services fetches it, `'ready'` once it can
|
|
41
|
+
* run, and `'unavailable'` when it could not be obtained within
|
|
42
|
+
* {@link MODEL_WAIT_MS} — no Play Services, no network, or a declined install.
|
|
43
|
+
*
|
|
44
|
+
* The flow primes the download at open (see `MyazaKYC.tsx`), so by the time the
|
|
45
|
+
* user reaches the step this is normally already `'ready'` and no waiting screen
|
|
46
|
+
* is ever shown.
|
|
47
|
+
*
|
|
48
|
+
* @param isReady whether the model can run right now
|
|
49
|
+
* @param prime starts the download; called again here because the step can be
|
|
50
|
+
* reached directly in a resumed flow, and priming is a no-op once ready
|
|
51
|
+
*/
|
|
52
|
+
export function useNativeModelReady(
|
|
53
|
+
isReady: () => boolean,
|
|
54
|
+
prime: () => void,
|
|
55
|
+
): ModelReadyState {
|
|
56
|
+
const [state, setState] = useState<ModelReadyState>(() =>
|
|
57
|
+
isReady() ? 'ready' : 'preparing',
|
|
58
|
+
);
|
|
59
|
+
const startedAt = useRef(Date.now());
|
|
60
|
+
// Refs, not deps: a caller passing inline arrows would otherwise restart the
|
|
61
|
+
// poll, and the wait clock with it, on every render.
|
|
62
|
+
const fns = useRef({ isReady, prime });
|
|
63
|
+
fns.current = { isReady, prime };
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (state !== 'preparing') return;
|
|
67
|
+
|
|
68
|
+
fns.current.prime();
|
|
69
|
+
|
|
70
|
+
const id = setInterval(() => {
|
|
71
|
+
if (fns.current.isReady()) {
|
|
72
|
+
setState('ready');
|
|
73
|
+
} else if (Date.now() - startedAt.current > MODEL_WAIT_MS) {
|
|
74
|
+
setState('unavailable');
|
|
75
|
+
}
|
|
76
|
+
}, POLL_INTERVAL_MS);
|
|
77
|
+
|
|
78
|
+
return () => clearInterval(id);
|
|
79
|
+
}, [state]);
|
|
80
|
+
|
|
81
|
+
return state;
|
|
82
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// The Proof of Address step's country gate.
|
|
3
|
+
//
|
|
4
|
+
// On the ADDRESS scope the declared country is the applicant's own claim about
|
|
5
|
+
// their market: it picks the document kinds on offer, the PoA vendor market and
|
|
6
|
+
// rides the submission as the verification's country, and the scope seeds none
|
|
7
|
+
// (see AddressCountryControl). A document uploaded with no country behind it is
|
|
8
|
+
// not yet a complete answer, so Continue holds until one is declared (user
|
|
9
|
+
// decision 2026-09-08). Elsewhere the flow's own country stands and the gate
|
|
10
|
+
// never bites; an org that accepts exactly ONE country has the control show it
|
|
11
|
+
// as a settled fact, which counts as declared.
|
|
12
|
+
//
|
|
13
|
+
// A THREE-WAY MIRROR of the web SDK's lib/poa-country-gate.ts and Flutter's
|
|
14
|
+
// config/poa_country_gate.dart; change the rule in one and change all three.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** Whether the Proof of Address step may continue as far as the COUNTRY is
|
|
18
|
+
* concerned (the upload has its own gate). */
|
|
19
|
+
export function poaCountryDeclared(facts: {
|
|
20
|
+
scope: string | null;
|
|
21
|
+
selectedCountry: string | null | undefined;
|
|
22
|
+
offered: readonly string[];
|
|
23
|
+
}): boolean {
|
|
24
|
+
if (facts.scope !== 'address') return true;
|
|
25
|
+
if (facts.selectedCountry?.trim()) return true;
|
|
26
|
+
return facts.offered.length === 1;
|
|
27
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { primeFaceModel } from '../liveness/visionCameraFaceDetector';
|
|
4
|
+
import { primeLivenessAvatars } from '../liveness/avatarSource';
|
|
5
|
+
import { primeTextModel } from '../mrz/textRecognizer';
|
|
6
|
+
|
|
7
|
+
// ─── Warming the on-device work the flow is about to need ────────────────────
|
|
8
|
+
//
|
|
9
|
+
// Android fetches ML Kit's models through Play Services rather than bundling
|
|
10
|
+
// them, and the gesture animations are served rather than shipped. All three
|
|
11
|
+
// are wanted in front of a camera, which is the worst possible moment to start
|
|
12
|
+
// a download — so they start the moment the flow opens instead, overlapping
|
|
13
|
+
// the screens the user is already reading (consent, ID type, the document
|
|
14
|
+
// step). Mirrors the web SDK's primeFaceMesh().
|
|
15
|
+
//
|
|
16
|
+
// Best-effort throughout: every step still gates on its own readiness, so a
|
|
17
|
+
// failure here costs a head start and nothing else.
|
|
18
|
+
//
|
|
19
|
+
// SHARED BY BOTH ENTRY POINTS, and that is the whole point of the file. This
|
|
20
|
+
// effect used to live inside the <MyazaKYC/> trigger component, so a consumer
|
|
21
|
+
// using useMyazaKYC() — the hook the SDK's own example uses, and the one
|
|
22
|
+
// documented for programmatic control — primed NOTHING. The face model was
|
|
23
|
+
// first requested when the liveness step mounted, in front of a camera the
|
|
24
|
+
// user was already looking at, and the gesture GIFs were fetched at the same
|
|
25
|
+
// moment. On a device that had never downloaded the model (a fresh install,
|
|
26
|
+
// the common case for a real applicant) that is a ~8 MB wait with the camera
|
|
27
|
+
// already up. A warm device hides it completely, which is why it survived:
|
|
28
|
+
// isModelReady() answers true on the first call and every path looks correct.
|
|
29
|
+
//
|
|
30
|
+
// The text model is the exception that proves it — useAutoCapture primes it at
|
|
31
|
+
// the document step, so it kept a head start on both paths regardless.
|
|
32
|
+
export function usePrimeModels(
|
|
33
|
+
wantOpen: boolean,
|
|
34
|
+
apiKey: string,
|
|
35
|
+
devUrl?: string,
|
|
36
|
+
): void {
|
|
37
|
+
// Once per open sequence: priming is idempotent, but re-running it on every
|
|
38
|
+
// render would ask Play Services the same question a few times a second.
|
|
39
|
+
const primedRef = useRef(false);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!wantOpen || primedRef.current) return;
|
|
42
|
+
primedRef.current = true;
|
|
43
|
+
primeFaceModel();
|
|
44
|
+
// The larger of the two, and wanted EARLIER in the flow than the face one
|
|
45
|
+
// (the document step comes before liveness), so it has the least time to
|
|
46
|
+
// arrive and the most to gain from the head start.
|
|
47
|
+
primeTextModel();
|
|
48
|
+
primeLivenessAvatars(apiKey, devUrl);
|
|
49
|
+
}, [wantOpen, apiKey, devUrl]);
|
|
50
|
+
}
|
package/src/lib/resubmit.ts
CHANGED
|
@@ -17,6 +17,30 @@ export interface ResubmitConfig {
|
|
|
17
17
|
steps: string[];
|
|
18
18
|
/** Reviewer's note to the applicant. */
|
|
19
19
|
message?: string | null;
|
|
20
|
+
/**
|
|
21
|
+
* The ID the verification being redone used, when the server CARRIES it.
|
|
22
|
+
*
|
|
23
|
+
* A redo keeps the verification id, so a reviewer who did not tick the ID
|
|
24
|
+
* has asked for nothing about it: the server keeps the original number,
|
|
25
|
+
* document photos, typed name and chip read, and says so by sending the
|
|
26
|
+
* idType here. Present only on an individual, single-ID send-back whose
|
|
27
|
+
* reviewer did not tick 'id-type'. Absent (an older server, a business redo,
|
|
28
|
+
* a multi-ID run, or the ID was ticked) keeps the old behaviour.
|
|
29
|
+
*/
|
|
30
|
+
idType?: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The ID a redo keeps, or null when the applicant must name one again.
|
|
35
|
+
*
|
|
36
|
+
* Read defensively rather than trusted: an idType beside a plan that ticks the
|
|
37
|
+
* ID picker, or beside no plan at all, is not an instruction to skip it.
|
|
38
|
+
*/
|
|
39
|
+
export function keptIdType(resubmit: ResubmitConfig | undefined | null): string | null {
|
|
40
|
+
const asked = resubmit?.steps;
|
|
41
|
+
if (!asked?.length || asked.includes('id-type')) return null;
|
|
42
|
+
const idType = resubmit?.idType;
|
|
43
|
+
return typeof idType === 'string' && idType.trim().length > 0 ? idType : null;
|
|
20
44
|
}
|
|
21
45
|
|
|
22
46
|
/**
|
|
@@ -48,10 +72,12 @@ const EVIDENCE: KYCStep[] = ['id-input', 'document-capture', 'nfc'];
|
|
|
48
72
|
* Steps a narrowed flow keeps regardless, because without them it cannot
|
|
49
73
|
* produce a submission at all.
|
|
50
74
|
*
|
|
51
|
-
*
|
|
52
|
-
* forward from the
|
|
53
|
-
* this is and supply it. `POST /verify` requires an `idType`, and a
|
|
54
|
-
* ID requires the number with it.
|
|
75
|
+
* Unless the server carries the original ID (`keptIdType`), nothing is carried
|
|
76
|
+
* forward from the attempt being redone, so the applicant must still say which
|
|
77
|
+
* ID this is and supply it. `POST /verify` requires an `idType`, and a
|
|
78
|
+
* number-only ID requires the number with it. When the ID IS carried, the
|
|
79
|
+
* identity steps drop out: the SDK preselects the kept ID and the server fills
|
|
80
|
+
* in the number, the document photos and the chip read.
|
|
55
81
|
*
|
|
56
82
|
* So narrowing removes the things arranged AROUND the identity — liveness,
|
|
57
83
|
* proof of address, the questionnaire, contact checks — and never the identity
|
|
@@ -93,9 +119,12 @@ export function applyResubmitSteps(
|
|
|
93
119
|
|
|
94
120
|
const wanted = new Set<string>(asked);
|
|
95
121
|
if (wantsEvidence) for (const step of EVIDENCE) wanted.add(step);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
122
|
+
// A kept ID needs no picker and, unless the reviewer asked for the evidence,
|
|
123
|
+
// no evidence step either: the redo is only what was ticked. KYB never keeps
|
|
124
|
+
// an ID (the server never sends one for a business redo), so it stays as is.
|
|
125
|
+
const isBusiness = order.includes('business-details');
|
|
126
|
+
const required = isBusiness ? BUSINESS_REQUIRED : keptIdType(resubmit) ? [] : INDIVIDUAL_REQUIRED;
|
|
127
|
+
for (const step of required) wanted.add(step);
|
|
99
128
|
|
|
100
129
|
const narrowed = order.filter((step) => wanted.has(step) || ALWAYS.includes(step));
|
|
101
130
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { VerificationOutcome } from './result-wait';
|
|
2
|
+
import type { BiometricCopyText } from '../config/biometricOptions';
|
|
3
|
+
|
|
4
|
+
// ─── What the terminal screens say ──────────────────────────────────────────
|
|
5
|
+
//
|
|
6
|
+
// Pure so it is testable without React. The server's own reason wins on a
|
|
7
|
+
// decline or an error when it sent one: it is written for the applicant.
|
|
8
|
+
// UK English, no em dashes (user-facing copy rule).
|
|
9
|
+
|
|
10
|
+
export type ResultTone = 'success' | 'error' | 'info';
|
|
11
|
+
|
|
12
|
+
export interface ResultCopy {
|
|
13
|
+
tone: ResultTone;
|
|
14
|
+
title: string;
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface WaitingCopy {
|
|
19
|
+
title: string;
|
|
20
|
+
description: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The ONE loading screen after the capture. On a re-authentication that waits
|
|
25
|
+
* for its verdict it spans the selfie upload, the submission and the poll, so
|
|
26
|
+
* it names the check rather than any of the three steps behind it. A retry in
|
|
27
|
+
* flight replaces the description, never the title: the person is still
|
|
28
|
+
* waiting for the same thing. `override` is the org's own words for the
|
|
29
|
+
* screen (lib/biometric-copy.ts), field by field over the default.
|
|
30
|
+
*/
|
|
31
|
+
export function describeWaiting(opts: {
|
|
32
|
+
scope: string | null;
|
|
33
|
+
waitsForResult: boolean;
|
|
34
|
+
retry?: { attempt: number; total: number } | null;
|
|
35
|
+
override?: BiometricCopyText | null;
|
|
36
|
+
}): WaitingCopy {
|
|
37
|
+
const base = withOverride(waitingCopyFor(opts.scope, opts.waitsForResult), opts.override);
|
|
38
|
+
if (opts.retry) {
|
|
39
|
+
return { title: base.title, description: `Connection issue, retrying (${opts.retry.attempt}/${opts.retry.total}).` };
|
|
40
|
+
}
|
|
41
|
+
return base;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function waitingCopyFor(scope: string | null, waitsForResult: boolean): WaitingCopy {
|
|
45
|
+
if (scope === 'biometric-authentication') {
|
|
46
|
+
return waitsForResult
|
|
47
|
+
? { title: "Checking it's you", description: 'Matching your selfie against the photo on record. This usually takes a few seconds.' }
|
|
48
|
+
: { title: 'Sending your face check', description: 'This only takes a moment.' };
|
|
49
|
+
}
|
|
50
|
+
if (scope === 'biometric-enrollment') {
|
|
51
|
+
return { title: 'Saving your selfie', description: 'It becomes the reference for your future face checks.' };
|
|
52
|
+
}
|
|
53
|
+
return { title: 'Submitting your verification', description: 'Please wait a moment.' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The org's own words for a screen, over the default, field by field. */
|
|
57
|
+
function withOverride<T extends { title: string; description: string }>(base: T, override?: BiometricCopyText | null): T {
|
|
58
|
+
if (!override) return base;
|
|
59
|
+
return {
|
|
60
|
+
...base,
|
|
61
|
+
...(override.title ? { title: override.title } : {}),
|
|
62
|
+
...(override.description ? { description: override.description } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** What the person is told, per outcome. The server's own reason wins on a
|
|
67
|
+
* decline or an error when it sent one; it is written for the applicant.
|
|
68
|
+
* `copy` is the org's own words for the two verdict screens: on a decline
|
|
69
|
+
* its description wins even over the server's reason, since the org chose
|
|
70
|
+
* to say that. */
|
|
71
|
+
export function describeOutcome(
|
|
72
|
+
outcome: VerificationOutcome,
|
|
73
|
+
copy?: { verified?: BiometricCopyText | null; declined?: BiometricCopyText | null },
|
|
74
|
+
): ResultCopy {
|
|
75
|
+
if (outcome.kind === 'timeout') {
|
|
76
|
+
return {
|
|
77
|
+
tone: 'info',
|
|
78
|
+
title: 'Still checking',
|
|
79
|
+
description: "This is taking longer than usual. You'll be notified as soon as it's done.",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
switch (outcome.status) {
|
|
83
|
+
case 'approved':
|
|
84
|
+
return withOverride(
|
|
85
|
+
{ tone: 'success', title: "You're verified", description: 'Your face matched the photo on record.' },
|
|
86
|
+
copy?.verified,
|
|
87
|
+
);
|
|
88
|
+
case 'declined':
|
|
89
|
+
return withOverride(
|
|
90
|
+
{
|
|
91
|
+
tone: 'error',
|
|
92
|
+
title: "We couldn't confirm it's you",
|
|
93
|
+
description: outcome.reason ?? "Your face didn't match the photo on record.",
|
|
94
|
+
},
|
|
95
|
+
copy?.declined,
|
|
96
|
+
);
|
|
97
|
+
case 'in_review':
|
|
98
|
+
return {
|
|
99
|
+
tone: 'info',
|
|
100
|
+
title: 'Under review',
|
|
101
|
+
description: "A reviewer will take a look. You'll be notified of the outcome.",
|
|
102
|
+
};
|
|
103
|
+
case 'error':
|
|
104
|
+
return {
|
|
105
|
+
tone: 'error',
|
|
106
|
+
title: 'Something went wrong',
|
|
107
|
+
description: outcome.reason ?? "We couldn't complete your check. Please try again in a moment.",
|
|
108
|
+
};
|
|
109
|
+
default:
|
|
110
|
+
return {
|
|
111
|
+
tone: 'info',
|
|
112
|
+
title: 'Check submitted',
|
|
113
|
+
description: "You'll be notified of the result.",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { SessionStatus, VerificationStatusResponse } from '../services/api-types';
|
|
2
|
+
|
|
3
|
+
// ─── Waiting for a verdict in the flow ──────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// The platform is fire-and-forget: /verify answers in milliseconds and the
|
|
6
|
+
// worker settles the check afterwards. A re-authentication is the one flow
|
|
7
|
+
// whose verdict the person is waiting for RIGHT THERE, so the submitted step
|
|
8
|
+
// polls the publishable status endpoint (state + reason, never result data)
|
|
9
|
+
// until the check leaves its pending states. Pure and injectable, like the
|
|
10
|
+
// presence watch wait: the screen owns nothing but the rendering.
|
|
11
|
+
|
|
12
|
+
export const RESULT_WAIT_MS = 60 * 1000;
|
|
13
|
+
export const RESULT_POLL_MS = 1500;
|
|
14
|
+
|
|
15
|
+
/** The states a submitted check passes through before it settles. */
|
|
16
|
+
const PENDING: ReadonlySet<SessionStatus> = new Set(['not_started', 'in_progress', 'processing']);
|
|
17
|
+
|
|
18
|
+
export type VerificationOutcome =
|
|
19
|
+
| { kind: 'settled'; status: SessionStatus; reason: string | null; reasonCode: string | null }
|
|
20
|
+
| { kind: 'timeout' };
|
|
21
|
+
|
|
22
|
+
export interface AwaitOutcomeDeps {
|
|
23
|
+
/** One status read; null on any failure (the wait keeps going). */
|
|
24
|
+
fetchStatus: () => Promise<VerificationStatusResponse | null>;
|
|
25
|
+
sleep?: (ms: number) => Promise<void>;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
waitMs?: number;
|
|
28
|
+
pollMs?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isPendingStatus(status: SessionStatus): boolean {
|
|
32
|
+
return PENDING.has(status);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Poll until the check settles or the budget runs out. A failed read is not
|
|
37
|
+
* a verdict: it is skipped and the next poll tries again, so a network blip
|
|
38
|
+
* mid-wait never reads as an outcome.
|
|
39
|
+
*/
|
|
40
|
+
export async function awaitVerificationOutcome(deps: AwaitOutcomeDeps): Promise<VerificationOutcome> {
|
|
41
|
+
const sleep = deps.sleep ?? ((ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
|
|
42
|
+
const now = deps.now ?? Date.now;
|
|
43
|
+
const deadline = now() + (deps.waitMs ?? RESULT_WAIT_MS);
|
|
44
|
+
const pollMs = deps.pollMs ?? RESULT_POLL_MS;
|
|
45
|
+
for (;;) {
|
|
46
|
+
const read = await deps.fetchStatus();
|
|
47
|
+
if (read && !isPendingStatus(read.status)) {
|
|
48
|
+
return { kind: 'settled', status: read.status, reason: read.reason ?? null, reasonCode: read.reasonCode ?? null };
|
|
49
|
+
}
|
|
50
|
+
if (now() >= deadline) return { kind: 'timeout' };
|
|
51
|
+
await sleep(pollMs);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The review card's map falls back in the SAME order on every SDK: the picture
|
|
2
|
+
// (the Maps Static API through the server), then the framed Google map the pin
|
|
3
|
+
// step drew, then the built-in tiles. Pure, so a test can pin the order — the
|
|
4
|
+
// Flutter card skipped the middle rung and confirmed the address on
|
|
5
|
+
// OpenStreetMap where RN confirmed it on Google (user report 2026-09-08).
|
|
6
|
+
// Mirrors kyc-sdk-flutter's reviewMapSurface; keep the two in lockstep.
|
|
7
|
+
|
|
8
|
+
export type ReviewMapSurface = 'stub' | 'picture' | 'pending' | 'framed' | 'builtIn';
|
|
9
|
+
|
|
10
|
+
export function reviewMapSurface(facts: {
|
|
11
|
+
vendorsStubbed: boolean;
|
|
12
|
+
/** The picture's bytes have landed. */
|
|
13
|
+
hasStaticMap: boolean;
|
|
14
|
+
/** The picture was refused (a project without the Static API 404s). */
|
|
15
|
+
staticMapFailed: boolean;
|
|
16
|
+
/** A server-minted maps frame URL this install can render. */
|
|
17
|
+
hasFrame: boolean;
|
|
18
|
+
}): ReviewMapSurface {
|
|
19
|
+
if (facts.vendorsStubbed) return 'stub';
|
|
20
|
+
if (facts.hasStaticMap) return 'picture';
|
|
21
|
+
// Nothing to draw yet and no verdict either: hold the picture's space rather
|
|
22
|
+
// than flashing the live map for the half second before the bytes land.
|
|
23
|
+
if (!facts.staticMapFailed) return 'pending';
|
|
24
|
+
if (facts.hasFrame) return 'framed';
|
|
25
|
+
return 'builtIn';
|
|
26
|
+
}
|
package/src/lib/scope.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Workflow SCOPE — what a flow verifies about the subject (mirror of the
|
|
2
|
+
// server's lib/workflows/scope.ts and the web SDK's lib/scope.ts; keep the
|
|
3
|
+
// three in lockstep). Absent = the full verification.
|
|
4
|
+
|
|
5
|
+
export type WorkflowScope =
|
|
6
|
+
| 'address'
|
|
7
|
+
| 'biometric-authentication'
|
|
8
|
+
| 'biometric-enrollment'
|
|
9
|
+
| 'questionnaire'
|
|
10
|
+
| 'contact';
|
|
11
|
+
|
|
12
|
+
/** The marker idType a scoped submission carries (the KYB product-in-idType
|
|
13
|
+
* convention — the server requires the matching published workflow). */
|
|
14
|
+
export const SCOPE_ID_TYPES: Record<WorkflowScope, string> = {
|
|
15
|
+
address: 'address',
|
|
16
|
+
'biometric-authentication': 'biometric-auth',
|
|
17
|
+
'biometric-enrollment': 'biometric-enroll',
|
|
18
|
+
questionnaire: 'questionnaire',
|
|
19
|
+
contact: 'contact',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function configScope(config: { scope?: WorkflowScope | string }): WorkflowScope | null {
|
|
23
|
+
const value = config.scope;
|
|
24
|
+
return value && value in SCOPE_ID_TYPES ? (value as WorkflowScope) : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The biometric scopes run the liveness capture; every other scope has no
|
|
28
|
+
* camera step at all. */
|
|
29
|
+
export function isFaceScope(scope: WorkflowScope | null): boolean {
|
|
30
|
+
return scope === 'biometric-authentication' || scope === 'biometric-enrollment';
|
|
31
|
+
}
|