@myazahq/kyc-sdk-react-native 2.5.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/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +6 -4
- package/app.plugin.js +89 -8
- package/ios/HybridMyazaFaceDetector.swift +8 -3
- package/package.json +14 -2
- package/src/MyazaBiometricAuth.tsx +119 -0
- 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/FramedMapPicker.tsx +122 -0
- package/src/components/GeoBadge.tsx +34 -0
- package/src/components/Icon.tsx +2 -185
- 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/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/StepView.tsx +17 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +42 -4
- 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/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 +11 -5
- 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/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/poa-country-gate.ts +27 -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/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/useLiveness.ts +6 -1
- 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/LivenessAvatar.tsx +15 -3
- package/src/screens/LivenessStep.tsx +154 -52
- 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/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/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-biometric.ts +37 -0
- package/src/services/api-types-biometric.ts +41 -0
- package/src/services/api-types.ts +93 -1
- package/src/services/api-verify-types.ts +37 -0
- package/src/services/api.ts +133 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/location.ts +234 -0
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +7 -0
- package/src/store/address.ts +97 -0
- package/src/store/derive.ts +29 -4
- package/src/store/kycStore.ts +112 -3
- 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 +48 -1
- package/src/types/verification.ts +12 -0
- package/src/types/workflow.ts +97 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import * as ImagePicker from 'expo-image-picker';
|
|
3
|
+
|
|
4
|
+
import type { ResolvedBusinessDocumentType } from '../config/businessSteps';
|
|
5
|
+
import { isAcceptedPoaMimeType, POA_ACCEPTED_MIME_TYPES } from '../config/proofOfAddress';
|
|
6
|
+
import { uploadSizeError } from '../config/uploadLimits';
|
|
7
|
+
import { loadDocumentPicker } from '../services/documentPicker';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Getting a company document off the device — the business-documents step's
|
|
11
|
+
// twin of usePoaAttach (extracted from BusinessDocumentsStep, 200-line rule).
|
|
12
|
+
// Three sources, one shape handed to the step's uploader. Every pick is judged
|
|
13
|
+
// against the shared upload caps BEFORE anything is compressed or sent, so
|
|
14
|
+
// the refusal names the file that was chosen (images 5 MB, PDFs 15 MB).
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export type AttachBusinessDocument = (
|
|
18
|
+
slot: ResolvedBusinessDocumentType,
|
|
19
|
+
uri: string,
|
|
20
|
+
mimeType: string | undefined,
|
|
21
|
+
name: string,
|
|
22
|
+
) => Promise<void>;
|
|
23
|
+
|
|
24
|
+
type Pick = (slot: ResolvedBusinessDocumentType) => Promise<void>;
|
|
25
|
+
|
|
26
|
+
export function useBusinessDocumentAttach(
|
|
27
|
+
attach: AttachBusinessDocument,
|
|
28
|
+
setError: (message: string) => void,
|
|
29
|
+
): { takePhoto: Pick; choosePhoto: Pick; chooseFile: Pick } {
|
|
30
|
+
const tooLarge = useCallback(
|
|
31
|
+
(mime: string | undefined, size: number | undefined): boolean => {
|
|
32
|
+
const message = uploadSizeError(mime, size);
|
|
33
|
+
if (message) setError(message);
|
|
34
|
+
return message !== null;
|
|
35
|
+
},
|
|
36
|
+
[setError],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const takePhoto = useCallback<Pick>(
|
|
40
|
+
async (slot) => {
|
|
41
|
+
const permission = await ImagePicker.requestCameraPermissionsAsync();
|
|
42
|
+
if (!permission.granted) {
|
|
43
|
+
setError('Camera access is needed to photograph the document.');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const result = await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], quality: 1 });
|
|
47
|
+
const asset = result.canceled ? undefined : result.assets[0];
|
|
48
|
+
if (!asset || tooLarge(asset.mimeType, asset.fileSize)) return;
|
|
49
|
+
// A FRIENDLY name, not the picker's temp junk — camera/library assets
|
|
50
|
+
// carry generated names; the slot key says what the file IS.
|
|
51
|
+
await attach(slot, asset.uri, asset.mimeType ?? 'image/jpeg', `${slot.key}.jpg`);
|
|
52
|
+
},
|
|
53
|
+
[attach, setError, tooLarge],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const choosePhoto = useCallback<Pick>(
|
|
57
|
+
async (slot) => {
|
|
58
|
+
const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], quality: 1 });
|
|
59
|
+
const asset = result.canceled ? undefined : result.assets[0];
|
|
60
|
+
if (!asset || tooLarge(asset.mimeType, asset.fileSize)) return;
|
|
61
|
+
await attach(slot, asset.uri, asset.mimeType ?? 'image/jpeg', `${slot.key}.jpg`);
|
|
62
|
+
},
|
|
63
|
+
[attach, tooLarge],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const chooseFile = useCallback<Pick>(
|
|
67
|
+
async (slot) => {
|
|
68
|
+
const picker = loadDocumentPicker();
|
|
69
|
+
if (!picker) {
|
|
70
|
+
setError('Choosing a file is not available in this app. Please photograph the document.');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const result = await picker.getDocumentAsync({
|
|
74
|
+
type: [...POA_ACCEPTED_MIME_TYPES],
|
|
75
|
+
copyToCacheDirectory: true,
|
|
76
|
+
multiple: false,
|
|
77
|
+
});
|
|
78
|
+
const asset = result.canceled ? undefined : result.assets?.[0];
|
|
79
|
+
if (!asset) return;
|
|
80
|
+
// The picker's `type` filter is advisory on some platforms, so what came
|
|
81
|
+
// back is re-checked rather than trusted.
|
|
82
|
+
if (!isAcceptedPoaMimeType(asset.mimeType)) {
|
|
83
|
+
setError('Please choose a PDF, JPG or PNG file.');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (tooLarge(asset.mimeType, asset.size)) return;
|
|
87
|
+
await attach(slot, asset.uri, asset.mimeType, asset.name ?? slot.key);
|
|
88
|
+
},
|
|
89
|
+
[attach, setError, tooLarge],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return { takePhoto, choosePhoto, chooseFile };
|
|
93
|
+
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { useCallback, useState } from 'react';
|
|
2
2
|
import * as ImagePicker from 'expo-image-picker';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
POA_ACCEPTED_MIME_TYPES,
|
|
7
|
-
POA_MAX_BYTES,
|
|
8
|
-
} from '../config/proofOfAddress';
|
|
4
|
+
import { isAcceptedPoaMimeType, POA_ACCEPTED_MIME_TYPES } from '../config/proofOfAddress';
|
|
5
|
+
import { uploadSizeError } from '../config/uploadLimits';
|
|
9
6
|
import { loadDocumentPicker } from '../services/documentPicker';
|
|
10
7
|
|
|
11
8
|
// ---------------------------------------------------------------------------
|
|
@@ -38,13 +35,13 @@ export function usePoaAttach(
|
|
|
38
35
|
takePhoto: () => Promise<void>;
|
|
39
36
|
chooseFile: () => Promise<void>;
|
|
40
37
|
} {
|
|
38
|
+
// Judged against the shared caps BEFORE anything is compressed or sent, so
|
|
39
|
+
// the refusal names the file that was chosen (images 5 MB, PDFs 15 MB).
|
|
41
40
|
const tooLarge = useCallback(
|
|
42
|
-
(size: number | undefined): boolean => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
return false;
|
|
41
|
+
(mime: string | undefined, size: number | undefined): boolean => {
|
|
42
|
+
const message = uploadSizeError(mime, size);
|
|
43
|
+
if (message) setError(message);
|
|
44
|
+
return message !== null;
|
|
48
45
|
},
|
|
49
46
|
[setError],
|
|
50
47
|
);
|
|
@@ -58,7 +55,7 @@ export function usePoaAttach(
|
|
|
58
55
|
const result = await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], quality: 1 });
|
|
59
56
|
const asset = result.canceled ? undefined : result.assets[0];
|
|
60
57
|
if (!asset) return;
|
|
61
|
-
if (tooLarge(asset.fileSize)) return;
|
|
58
|
+
if (tooLarge(asset.mimeType, asset.fileSize)) return;
|
|
62
59
|
await upload({
|
|
63
60
|
uri: asset.uri,
|
|
64
61
|
mimeType: asset.mimeType ?? 'image/jpeg',
|
|
@@ -73,7 +70,7 @@ export function usePoaAttach(
|
|
|
73
70
|
});
|
|
74
71
|
const asset = result.canceled ? undefined : result.assets[0];
|
|
75
72
|
if (!asset) return;
|
|
76
|
-
if (tooLarge(asset.fileSize)) return;
|
|
73
|
+
if (tooLarge(asset.mimeType, asset.fileSize)) return;
|
|
77
74
|
await upload({
|
|
78
75
|
uri: asset.uri,
|
|
79
76
|
mimeType: asset.mimeType ?? 'image/jpeg',
|
|
@@ -97,10 +94,10 @@ export function usePoaAttach(
|
|
|
97
94
|
// The picker's `type` filter is advisory on some platforms, so what came
|
|
98
95
|
// back is re-checked rather than trusted.
|
|
99
96
|
if (!isAcceptedPoaMimeType(asset.mimeType)) {
|
|
100
|
-
setError('Please choose a
|
|
97
|
+
setError('Please choose a PDF, JPG or PNG file.');
|
|
101
98
|
return;
|
|
102
99
|
}
|
|
103
|
-
if (tooLarge(asset.size)) return;
|
|
100
|
+
if (tooLarge(asset.mimeType, asset.size)) return;
|
|
104
101
|
await upload({
|
|
105
102
|
uri: asset.uri,
|
|
106
103
|
mimeType: asset.mimeType,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BiometricAuthRequest,
|
|
3
|
+
BiometricAuthResponse,
|
|
4
|
+
BiometricStatusResponse,
|
|
5
|
+
} from './api-types-biometric';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// The biometric re-authentication calls, split out of createKYCApi (200-line
|
|
9
|
+
// rule). Given the client's own `request`, so they ride the same base URL,
|
|
10
|
+
// bearer key, SDK-version header and error mapping as everything else.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export type JsonRequest = <T>(path: string, init?: RequestInit) => Promise<T>;
|
|
14
|
+
|
|
15
|
+
export function biometricCalls(request: JsonRequest) {
|
|
16
|
+
return {
|
|
17
|
+
/**
|
|
18
|
+
* Re-authenticate a verified user by matching a live selfie 1:1 against
|
|
19
|
+
* their KYC enrollment reference. Publishable-safe. Uniform 404
|
|
20
|
+
* `not_enrolled` — a missing entity, a business entity and no template
|
|
21
|
+
* all answer the same, so nothing can be probed.
|
|
22
|
+
*/
|
|
23
|
+
async authenticate(body: BiometricAuthRequest): Promise<BiometricAuthResponse> {
|
|
24
|
+
return request<BiometricAuthResponse>('/biometric/authenticate', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
body: JSON.stringify(body),
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/** Whether a user is enrolled for face re-auth (whether to OFFER it). */
|
|
31
|
+
async getBiometricStatus(externalUserId: string): Promise<BiometricStatusResponse> {
|
|
32
|
+
return request<BiometricStatusResponse>(
|
|
33
|
+
`/biometric/status/${encodeURIComponent(externalUserId)}`,
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Biometric re-authentication wire shapes — a mirror of the web SDK's
|
|
3
|
+
// services/api.ts (keep the two in lockstep). Publishable-safe: the verdict,
|
|
4
|
+
// a confidence, and a single-use proof token. No PII.
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
/** What the SDK asserts about the liveness run that produced the selfie. */
|
|
8
|
+
export interface BiometricLivenessClaim {
|
|
9
|
+
mode: 'gestures' | 'flash' | 'both';
|
|
10
|
+
passed: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Request body for `POST /api/kyc/biometric/authenticate`. */
|
|
14
|
+
export interface BiometricAuthRequest {
|
|
15
|
+
/** The org's user reference (Entity.externalUserId) being re-authenticated. */
|
|
16
|
+
externalUserId: string;
|
|
17
|
+
/** A mediaId from `upload(file, 'selfie')` — the live selfie. */
|
|
18
|
+
selfie: string;
|
|
19
|
+
liveness?: BiometricLivenessClaim;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Response from `POST /api/kyc/biometric/authenticate`. `token` is present
|
|
24
|
+
* only on `authenticated` — redeem it from your backend with a secret key at
|
|
25
|
+
* `/biometric/verify-proof`.
|
|
26
|
+
*/
|
|
27
|
+
export interface BiometricAuthResponse {
|
|
28
|
+
authenticated: boolean;
|
|
29
|
+
status: 'authenticated' | 'no_match' | 'liveness_failed';
|
|
30
|
+
confidence: number | null;
|
|
31
|
+
live: boolean;
|
|
32
|
+
attemptId: string;
|
|
33
|
+
token?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Response from `GET /api/kyc/biometric/status/:externalUserId`. */
|
|
37
|
+
export interface BiometricStatusResponse {
|
|
38
|
+
enrolled: boolean;
|
|
39
|
+
enrolledAt?: string;
|
|
40
|
+
lastAuthenticatedAt?: string | null;
|
|
41
|
+
}
|
|
@@ -23,7 +23,9 @@ export type MediaUploadType =
|
|
|
23
23
|
// The two kinds that accept a PDF — a proof of address is usually a
|
|
24
24
|
// downloaded statement, and company paperwork is almost always a scan.
|
|
25
25
|
| 'proof_of_address'
|
|
26
|
-
| 'business_document'
|
|
26
|
+
| 'business_document'
|
|
27
|
+
// Address Intelligence door / premises photo (image only).
|
|
28
|
+
| 'address_photo';
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* A file to upload, in React Native's multipart shape. `uri` points at a
|
|
@@ -142,6 +144,80 @@ export interface SdkConfigResponse {
|
|
|
142
144
|
* carries the same lookup as a RISK signal, and the two must not be confused.
|
|
143
145
|
*/
|
|
144
146
|
geoCountry?: string | null;
|
|
147
|
+
/** Whether the platform has a forward-search backend. Gates the whole
|
|
148
|
+
* address-search step; absent means no search screen, never an error. */
|
|
149
|
+
addressSearch?: boolean;
|
|
150
|
+
/** Which backend: 'autocomplete' (Places, as-you-type) or 'basic'
|
|
151
|
+
* (explicit submit). Absent when addressSearch is false. */
|
|
152
|
+
addressSearchMode?: 'autocomplete' | 'basic';
|
|
153
|
+
/**
|
|
154
|
+
* The framed Google-map picker page (OUR hosted /embed/map + a signed APP
|
|
155
|
+
* grant), for a WebView. Absent when the platform holds no Maps key — the
|
|
156
|
+
* built-in OSM picker is the fallback every map failure degrades to.
|
|
157
|
+
*/
|
|
158
|
+
mapsFrameUrl?: string | null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Address search / reverse geocoding ──────────────────────────────────────
|
|
162
|
+
//
|
|
163
|
+
// Every one of these may fail, and every failure degrades to "place the pin by
|
|
164
|
+
// hand" — never to a blocked flow. Shapes mirror the web SDK's services/api.ts.
|
|
165
|
+
|
|
166
|
+
/** The pin's address broken down — what the details sheet displays as rows. */
|
|
167
|
+
export interface AddressParts {
|
|
168
|
+
street?: string | null;
|
|
169
|
+
area?: string | null;
|
|
170
|
+
city?: string | null;
|
|
171
|
+
state?: string | null;
|
|
172
|
+
postcode?: string | null;
|
|
173
|
+
/**
|
|
174
|
+
* ISO-2 of the pin's OWN country, from the geocoder. On the address scope
|
|
175
|
+
* the declared country follows it (lib/country-adoption.ts); it is never a
|
|
176
|
+
* row in the sheet, which shows the country the flow declared.
|
|
177
|
+
*/
|
|
178
|
+
country?: string | null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface AddressReverseResult {
|
|
182
|
+
line: string | null;
|
|
183
|
+
road: string | null;
|
|
184
|
+
parts?: AddressParts | null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** One candidate from the basic (explicit-submit) forward search. */
|
|
188
|
+
export interface AddressSearchHit {
|
|
189
|
+
label: string;
|
|
190
|
+
lat: number;
|
|
191
|
+
lng: number;
|
|
192
|
+
houseNumber: string | null;
|
|
193
|
+
road: string | null;
|
|
194
|
+
/** ISO-2 of the hit's own country (the declaration derives from it). */
|
|
195
|
+
country?: string | null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** One Places autocomplete suggestion. */
|
|
199
|
+
export interface PlaceSuggestion {
|
|
200
|
+
placeId: string;
|
|
201
|
+
mainText: string;
|
|
202
|
+
secondaryText: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A picked suggestion, resolved to coordinates + structured pieces. */
|
|
206
|
+
export interface ResolvedPlace {
|
|
207
|
+
lat: number;
|
|
208
|
+
lng: number;
|
|
209
|
+
houseNumber: string | null;
|
|
210
|
+
road: string | null;
|
|
211
|
+
formatted: string | null;
|
|
212
|
+
area?: string | null;
|
|
213
|
+
city?: string | null;
|
|
214
|
+
state?: string | null;
|
|
215
|
+
postcode?: string | null;
|
|
216
|
+
/**
|
|
217
|
+
* ISO-2 of the picked address's own country — the declaration derives from
|
|
218
|
+
* it on the address scope (a pick is the applicant's own statement).
|
|
219
|
+
*/
|
|
220
|
+
country?: string | null;
|
|
145
221
|
}
|
|
146
222
|
|
|
147
223
|
/**
|
|
@@ -182,6 +258,20 @@ export interface WorkflowResolutionResponse {
|
|
|
182
258
|
* carries the same lookup as a RISK signal, and the two must not be confused.
|
|
183
259
|
*/
|
|
184
260
|
geoCountry?: string | null;
|
|
261
|
+
/**
|
|
262
|
+
* The address-search availability flags, when the resolution route carries
|
|
263
|
+
* them.
|
|
264
|
+
*
|
|
265
|
+
* A `workflowId` mount skips `/config` entirely, so these are the only route
|
|
266
|
+
* by which such a mount could learn a search backend exists. The server does
|
|
267
|
+
* not serve them here yet (only `/config` and the hosted bootstrap do), which
|
|
268
|
+
* is why they degrade to "no search screen" rather than an error.
|
|
269
|
+
*/
|
|
270
|
+
addressSearch?: boolean;
|
|
271
|
+
addressSearchMode?: 'autocomplete' | 'basic';
|
|
272
|
+
/** The framed Google-map picker page, when the route carries one (see
|
|
273
|
+
* SdkConfigResponse.mapsFrameUrl). */
|
|
274
|
+
mapsFrameUrl?: string | null;
|
|
185
275
|
/** KYB only: the mapped applicant workflow, when configured and resolvable. */
|
|
186
276
|
applicantWorkflow?: ApplicantWorkflowPayload | null;
|
|
187
277
|
}
|
|
@@ -318,3 +408,5 @@ export interface BusinessSearchResponse {
|
|
|
318
408
|
export interface BusinessRegionsResponse {
|
|
319
409
|
regions: { code: string; name: string }[];
|
|
320
410
|
}
|
|
411
|
+
|
|
412
|
+
export * from './api-types-biometric';
|
|
@@ -45,9 +45,46 @@ export interface VerifyRequest {
|
|
|
45
45
|
documentBackVideo?: string;
|
|
46
46
|
livenessVideo?: string;
|
|
47
47
|
proofOfAddress?: string;
|
|
48
|
+
/** Address Intelligence door photo (individual flows only). */
|
|
49
|
+
addressPhoto?: string;
|
|
48
50
|
};
|
|
49
51
|
/** Which kind of document the user said they uploaded as proof of address. */
|
|
50
52
|
proofOfAddressType?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Address Intelligence: the map pin the applicant dropped (+ the optional
|
|
55
|
+
* one-shot device fix taken at Continue). The server corroborates it; the
|
|
56
|
+
* SDK only collects. Sent only when the step ran and a pin was confirmed.
|
|
57
|
+
*/
|
|
58
|
+
address?: {
|
|
59
|
+
lat: number;
|
|
60
|
+
lng: number;
|
|
61
|
+
accuracy?: number;
|
|
62
|
+
/**
|
|
63
|
+
* The line the applicant CONFIRMED (a search pick, or a reverse geocode
|
|
64
|
+
* they accepted). The server prefers it for the composed address over its
|
|
65
|
+
* own derivation, whose OSM coverage drops whole streets in our markets.
|
|
66
|
+
*/
|
|
67
|
+
label?: string;
|
|
68
|
+
directions?: string;
|
|
69
|
+
propertyName?: string;
|
|
70
|
+
propertyNumber?: string;
|
|
71
|
+
/** A street the applicant typed because no map source knew it. */
|
|
72
|
+
street?: string;
|
|
73
|
+
/** The edit-details claims: unit + area/region corrections. */
|
|
74
|
+
unit?: string;
|
|
75
|
+
neighbourhood?: string;
|
|
76
|
+
city?: string;
|
|
77
|
+
state?: string;
|
|
78
|
+
postcode?: string;
|
|
79
|
+
/** Street View entrance frame — coordinates only; the server fetches the
|
|
80
|
+
* image with its own key. Never written by this SDK (no panorama on
|
|
81
|
+
* mobile), but a session begun on a hosted page can carry one. */
|
|
82
|
+
streetView?: { panoId: string; heading: number; pitch: number; fov: number };
|
|
83
|
+
deviceLat?: number;
|
|
84
|
+
deviceLng?: number;
|
|
85
|
+
deviceAccuracy?: number;
|
|
86
|
+
capturedAt?: string;
|
|
87
|
+
};
|
|
51
88
|
/**
|
|
52
89
|
* Business (KYB) registry details. Its presence is what makes this a business
|
|
53
90
|
* submission — and the server REQUIRES a published KYB workflow for one, so
|
package/src/services/api.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { SDK_VERSION } from './deviceMetadata';
|
|
2
2
|
import type {
|
|
3
|
+
AddressReverseResult,
|
|
4
|
+
AddressSearchHit,
|
|
3
5
|
BusinessRegionsResponse,
|
|
4
6
|
BusinessSearchResponse,
|
|
5
7
|
BusinessSelectResponse,
|
|
@@ -8,6 +10,8 @@ import type {
|
|
|
8
10
|
ContactSendResponse,
|
|
9
11
|
HealthResponse,
|
|
10
12
|
MediaUploadType,
|
|
13
|
+
PlaceSuggestion,
|
|
14
|
+
ResolvedPlace,
|
|
11
15
|
SdkConfigResponse,
|
|
12
16
|
SessionStartResponse,
|
|
13
17
|
SessionSummaryResponse,
|
|
@@ -18,6 +22,7 @@ import type {
|
|
|
18
22
|
VerifyResponse,
|
|
19
23
|
WorkflowResolutionResponse,
|
|
20
24
|
} from './api-types';
|
|
25
|
+
import { biometricCalls } from './api-biometric';
|
|
21
26
|
|
|
22
27
|
// The HTTP contract lives in ./api-types and is re-exported here, so importers
|
|
23
28
|
// keep a single entry point for both the client and the shapes it exchanges.
|
|
@@ -70,9 +75,14 @@ function uriToBlob(uri: string, mimeType: string): Promise<Blob> {
|
|
|
70
75
|
xhr.onload = () => {
|
|
71
76
|
const blob = xhr.response as Blob | null;
|
|
72
77
|
if (blob) {
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
78
|
+
// ALWAYS re-type the part, never only when the platform inferred
|
|
79
|
+
// nothing. Its guess comes from the URI's extension and describes the
|
|
80
|
+
// file we PICKED, not the one we are sending: an iPhone gallery photo
|
|
81
|
+
// arrives as HEIC, gets transcoded to JPEG on the way here, and the
|
|
82
|
+
// part still went out labelled image/heic — which the server refuses,
|
|
83
|
+
// so most camera-roll uploads failed with a message that blamed the
|
|
84
|
+
// document. `mimeType` is the normalised type the bytes actually are.
|
|
85
|
+
resolve(blob.type === mimeType ? blob : blob.slice(0, blob.size, mimeType));
|
|
76
86
|
} else {
|
|
77
87
|
reject(new Error(`Could not read file at ${uri}`));
|
|
78
88
|
}
|
|
@@ -157,6 +167,8 @@ export function createKYCApi(baseUrl: string, apiKey: string) {
|
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
return {
|
|
170
|
+
...biometricCalls(request),
|
|
171
|
+
|
|
160
172
|
/**
|
|
161
173
|
* Single multipart upload: the local file is POSTed to our server, which
|
|
162
174
|
* stores it and returns the `mediaId` referenced later by /verify.
|
|
@@ -279,6 +291,10 @@ export function createKYCApi(baseUrl: string, apiKey: string) {
|
|
|
279
291
|
workflowId?: string;
|
|
280
292
|
/** Persistent device id — the anonymous-mount resume fallback. */
|
|
281
293
|
deviceRef?: string;
|
|
294
|
+
/** The same device block the submission sends, so the dashboard's
|
|
295
|
+
* in-progress row shows the device and SDK from the moment the SDK
|
|
296
|
+
* loads rather than after the applicant finishes (2026-09-08). */
|
|
297
|
+
device?: Record<string, unknown>;
|
|
282
298
|
}): Promise<SessionStartResponse> {
|
|
283
299
|
return request<SessionStartResponse>('/session/start', {
|
|
284
300
|
method: 'POST',
|
|
@@ -354,6 +370,120 @@ export function createKYCApi(baseUrl: string, apiKey: string) {
|
|
|
354
370
|
});
|
|
355
371
|
},
|
|
356
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Forward address search for the address step's search box.
|
|
375
|
+
*
|
|
376
|
+
* EXPLICIT SUBMIT ONLY — never call this per keystroke. The server's map
|
|
377
|
+
* source forbids autocomplete, and the request budget has to be spent on
|
|
378
|
+
* the query the person actually meant rather than on every prefix of it.
|
|
379
|
+
*/
|
|
380
|
+
async addressSearch(
|
|
381
|
+
query: string,
|
|
382
|
+
country?: string | null,
|
|
383
|
+
): Promise<{ results: AddressSearchHit[] }> {
|
|
384
|
+
const qs = new URLSearchParams({ q: query });
|
|
385
|
+
if (country) qs.set('country', country);
|
|
386
|
+
return request<{ results: AddressSearchHit[] }>(`/address/search?${qs.toString()}`);
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The framed Street View entrance, as an image SOURCE the review card can
|
|
391
|
+
* hand straight to <Image>.
|
|
392
|
+
*
|
|
393
|
+
* The browser key never reaches this SDK (the framed page holds it), so
|
|
394
|
+
* the picture comes through the server, which does. Returned as a URL plus
|
|
395
|
+
* the auth header rather than fetched bytes: React Native's Image carries
|
|
396
|
+
* headers itself, which keeps Blob, FileReader and base64 out of a path
|
|
397
|
+
* that only has to draw a thumbnail. Mirrors the web SDK's
|
|
398
|
+
* addressStreetViewPreview, which returns a Blob because a browser <img>
|
|
399
|
+
* cannot send an Authorization header.
|
|
400
|
+
*/
|
|
401
|
+
/**
|
|
402
|
+
* The pinned location as a PICTURE, for the review card.
|
|
403
|
+
*
|
|
404
|
+
* A confirmation screen wants a photograph of the place, not a second
|
|
405
|
+
* instrument: a live map there invites a drag that goes nowhere and
|
|
406
|
+
* carries the vendor's own controls over the SDK's chrome. Same shape as
|
|
407
|
+
* the Street View source above, and the same reason for it: the key lives
|
|
408
|
+
* on the server. lib/authed-image fetches it with the header — an <Image>
|
|
409
|
+
* given `source.headers` drops them on Android.
|
|
410
|
+
*/
|
|
411
|
+
staticMapSource(view: {
|
|
412
|
+
lat: number;
|
|
413
|
+
lng: number;
|
|
414
|
+
zoom?: number;
|
|
415
|
+
width?: number;
|
|
416
|
+
height?: number;
|
|
417
|
+
}): { uri: string; headers: Record<string, string> } {
|
|
418
|
+
const qs = new URLSearchParams({
|
|
419
|
+
lat: String(view.lat),
|
|
420
|
+
lng: String(view.lng),
|
|
421
|
+
zoom: String(view.zoom ?? 16),
|
|
422
|
+
width: String(Math.round(view.width ?? 640)),
|
|
423
|
+
height: String(Math.round(view.height ?? 360)),
|
|
424
|
+
});
|
|
425
|
+
return { uri: `${base}/address/static-map?${qs.toString()}`, headers };
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
streetViewPreviewSource(frame: {
|
|
429
|
+
panoId: string;
|
|
430
|
+
heading: number;
|
|
431
|
+
pitch: number;
|
|
432
|
+
fov: number;
|
|
433
|
+
}): { uri: string; headers: Record<string, string> } {
|
|
434
|
+
const qs = new URLSearchParams({
|
|
435
|
+
panoId: frame.panoId,
|
|
436
|
+
heading: String(frame.heading),
|
|
437
|
+
pitch: String(frame.pitch),
|
|
438
|
+
fov: String(frame.fov),
|
|
439
|
+
});
|
|
440
|
+
return {
|
|
441
|
+
uri: `${base}/address/street-view-preview?${qs.toString()}`,
|
|
442
|
+
headers,
|
|
443
|
+
};
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
/** The street line for a pin, for the summary card after a locate or a
|
|
447
|
+
* drag. Display only — it never decides anything. */
|
|
448
|
+
async addressReverse(lat: number, lng: number): Promise<AddressReverseResult> {
|
|
449
|
+
const qs = new URLSearchParams({ lat: String(lat), lng: String(lng) });
|
|
450
|
+
return request<AddressReverseResult>(`/address/reverse?${qs.toString()}`);
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Places-backed as-you-type suggestions.
|
|
455
|
+
*
|
|
456
|
+
* `session` is ONE token per typing session: it is the billing unit, so
|
|
457
|
+
* Google bills per session rather than per keystroke. Mint it when the
|
|
458
|
+
* search screen opens, reuse it for every keystroke, and mint a fresh one
|
|
459
|
+
* after a details call — that call closes the session.
|
|
460
|
+
*/
|
|
461
|
+
async addressAutocomplete(
|
|
462
|
+
query: string,
|
|
463
|
+
session: string,
|
|
464
|
+
country?: string | null,
|
|
465
|
+
near?: { lat: number; lng: number } | null,
|
|
466
|
+
): Promise<{ suggestions: PlaceSuggestion[] }> {
|
|
467
|
+
const qs = new URLSearchParams({ q: query, session });
|
|
468
|
+
if (country) qs.set('country', country);
|
|
469
|
+
// The device fix, a RANKING bias so nearby streets come first: without
|
|
470
|
+
// it "Awolowo Road" in Calabar ranks against every Awolowo Road in the
|
|
471
|
+
// country. Mirrors the web SDK's api.addressAutocomplete.
|
|
472
|
+
if (near) {
|
|
473
|
+
qs.set('lat', String(near.lat));
|
|
474
|
+
qs.set('lng', String(near.lng));
|
|
475
|
+
}
|
|
476
|
+
return request<{ suggestions: PlaceSuggestion[] }>(`/address/autocomplete?${qs.toString()}`);
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
/** Resolve a picked suggestion to coordinates + structured pieces. */
|
|
480
|
+
async addressPlace(placeId: string, session: string): Promise<{ place: ResolvedPlace }> {
|
|
481
|
+
const qs = new URLSearchParams({ session });
|
|
482
|
+
return request<{ place: ResolvedPlace }>(
|
|
483
|
+
`/address/place/${encodeURIComponent(placeId)}?${qs.toString()}`,
|
|
484
|
+
);
|
|
485
|
+
},
|
|
486
|
+
|
|
357
487
|
async health(): Promise<HealthResponse> {
|
|
358
488
|
// Public endpoint — no auth needed, but the shared headers are harmless.
|
|
359
489
|
return request<HealthResponse>('/health');
|
|
@@ -15,7 +15,7 @@ export type DeviceType = 'mobile' | 'tablet' | 'desktop' | 'unknown';
|
|
|
15
15
|
* Single source of truth for the SDK version — also used by `services/api.ts`
|
|
16
16
|
* for the `X-SDK-Version` header. Keep in sync with `package.json`.
|
|
17
17
|
*/
|
|
18
|
-
export const SDK_VERSION = '2.
|
|
18
|
+
export const SDK_VERSION = '2.6.0';
|
|
19
19
|
|
|
20
20
|
export interface ReactNativeDeviceMetadata {
|
|
21
21
|
sdkType: 'react-native';
|