@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,25 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, Easing } from 'react-native';
|
|
3
|
+
import { StyleAbsFill } from './constants';
|
|
4
|
+
|
|
5
|
+
// The shutter: a white flash over the camera circle as the still is taken,
|
|
6
|
+
// fading out over the beat the ring takes to close and go green. Mirrors the
|
|
7
|
+
// web SDK's animate-capture-flash (600ms ease-out). Core Animated on the
|
|
8
|
+
// native driver — opacity only — is the same recipe LightingBanner uses.
|
|
9
|
+
export function ShutterFlash() {
|
|
10
|
+
const opacity = useRef(new Animated.Value(0.85)).current;
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
Animated.timing(opacity, {
|
|
13
|
+
toValue: 0,
|
|
14
|
+
duration: 600,
|
|
15
|
+
easing: Easing.out(Easing.cubic),
|
|
16
|
+
useNativeDriver: true,
|
|
17
|
+
}).start();
|
|
18
|
+
}, [opacity]);
|
|
19
|
+
return (
|
|
20
|
+
<Animated.View
|
|
21
|
+
pointerEvents="none"
|
|
22
|
+
style={[StyleAbsFill, { backgroundColor: '#FFFFFF', opacity }]}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -3,6 +3,10 @@ export { StyleAbsFill, FRESH_FACE_TIMEOUT_MS, INSTRUCTION_HEIGHT } from './const
|
|
|
3
3
|
export { LightingBanner } from './LightingBanner';
|
|
4
4
|
export { resolveGuidance } from './resolveGuidance';
|
|
5
5
|
export { ProgressDots } from './ProgressDots';
|
|
6
|
+
export { CaptureRing } from './CaptureRing';
|
|
7
|
+
export { ShutterFlash } from './ShutterFlash';
|
|
6
8
|
export { SelfiePreview } from './SelfiePreview';
|
|
7
9
|
export { useSelfieUpload } from './useSelfieUpload';
|
|
8
10
|
export { LivenessComplete, LivenessFailed } from './LivenessOutcome';
|
|
11
|
+
export { LivenessCamera } from './LivenessCamera';
|
|
12
|
+
export { LivenessHandover, useSelfieAutoAdvance } from './LivenessHandover';
|
|
@@ -44,12 +44,29 @@ export function useSelfieUpload(): SelfieUpload {
|
|
|
44
44
|
const config = useKycConfig();
|
|
45
45
|
const api = useKyc((s) => s.api);
|
|
46
46
|
const setMediaId = useKyc((s) => s.setMediaId);
|
|
47
|
+
const setSelfiePreview = useKyc((s) => s.setSelfiePreview);
|
|
48
|
+
// The store-side progress report. The liveness step may hand over before
|
|
49
|
+
// the upload lands (the biometric scopes hide the review), so the submitted
|
|
50
|
+
// step waits on THIS rather than on this hook's local state.
|
|
51
|
+
const setSelfieUpload = useKyc((s) => s.setSelfieUpload);
|
|
52
|
+
const storedPreview = useKyc((s) => s.selfiePreviewUri);
|
|
53
|
+
const storedSelfieId = useKyc((s) => s.mediaIds.selfie);
|
|
47
54
|
|
|
48
|
-
|
|
55
|
+
// Hydrated from the STORE so leaving the step and returning lands on the
|
|
56
|
+
// review screen with the same selfie, exactly as document captures do. The
|
|
57
|
+
// setter writes through, keeping the store the source of truth.
|
|
58
|
+
const [selfieUri, setSelfieUriState] = useState<string | null>(storedPreview);
|
|
59
|
+
const setSelfieUri = useCallback(
|
|
60
|
+
(uri: string | null) => {
|
|
61
|
+
setSelfieUriState(uri);
|
|
62
|
+
setSelfiePreview(uri);
|
|
63
|
+
},
|
|
64
|
+
[setSelfiePreview],
|
|
65
|
+
);
|
|
49
66
|
const [uploading, setUploading] = useState(false);
|
|
50
67
|
const [retryInfo, setRetryInfo] = useState<{ attempt: number; total: number } | null>(null);
|
|
51
68
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
|
52
|
-
const selfieIdRef = useRef<string | null>(null);
|
|
69
|
+
const selfieIdRef = useRef<string | null>(storedSelfieId ?? null);
|
|
53
70
|
const videoPathRef = useRef<string | null>(null);
|
|
54
71
|
|
|
55
72
|
const uploadSelfieAndVideo = useCallback(
|
|
@@ -57,6 +74,7 @@ export function useSelfieUpload(): SelfieUpload {
|
|
|
57
74
|
setUploading(true);
|
|
58
75
|
setUploadError(null);
|
|
59
76
|
setRetryInfo(null);
|
|
77
|
+
setSelfieUpload({ status: 'uploading', message: null });
|
|
60
78
|
const onRetry = (attempt: number, total: number) => setRetryInfo({ attempt, total });
|
|
61
79
|
try {
|
|
62
80
|
const selfieId = await withRetry(
|
|
@@ -81,16 +99,18 @@ export function useSelfieUpload(): SelfieUpload {
|
|
|
81
99
|
}
|
|
82
100
|
setRetryInfo(null);
|
|
83
101
|
setUploading(false);
|
|
102
|
+
setSelfieUpload({ status: 'done', message: null });
|
|
84
103
|
} catch (err) {
|
|
85
104
|
setRetryInfo(null);
|
|
86
105
|
setUploading(false);
|
|
87
106
|
const kycError = mapToKycError(err, 'upload');
|
|
88
107
|
setUploadError(kycError.message);
|
|
108
|
+
setSelfieUpload({ status: 'failed', message: kycError.message });
|
|
89
109
|
toast.show({ variant: 'error', title: 'Upload failed', message: kycError.message });
|
|
90
110
|
safeReportError(config.onError, kycError);
|
|
91
111
|
}
|
|
92
112
|
},
|
|
93
|
-
[api, setMediaId, config.onError, toast],
|
|
113
|
+
[api, setMediaId, setSelfieUpload, config.onError, toast],
|
|
94
114
|
);
|
|
95
115
|
|
|
96
116
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { AccessibilityInfo, Animated, Easing, View } from 'react-native';
|
|
3
|
-
import Svg, { Circle,
|
|
3
|
+
import Svg, { Circle, G, Path, Rect } from 'react-native-svg';
|
|
4
4
|
|
|
5
5
|
import { useTheme } from '../../components/runtime';
|
|
6
6
|
|
|
@@ -112,22 +112,6 @@ export function NfcScanIllustration(): React.ReactElement {
|
|
|
112
112
|
// Same footprint as the web: full width up to 384 (`max-w-sm`), 320:240.
|
|
113
113
|
<View style={{ width: '100%', maxWidth: 384, aspectRatio: 320 / 240, alignSelf: 'center' }}>
|
|
114
114
|
<Svg width="100%" height="100%" viewBox="0 0 320 240">
|
|
115
|
-
<Defs>
|
|
116
|
-
{/* The field falls off with distance — the reason the document has
|
|
117
|
-
to be held CLOSE, said as a gradient instead of a caption. */}
|
|
118
|
-
<RadialGradient
|
|
119
|
-
id="kyc-nfc-field"
|
|
120
|
-
cx={COUPLING.x}
|
|
121
|
-
cy={COUPLING.y}
|
|
122
|
-
r={90}
|
|
123
|
-
gradientUnits="userSpaceOnUse"
|
|
124
|
-
>
|
|
125
|
-
<Stop offset="0" stopColor={colors.primary} stopOpacity={0.18} />
|
|
126
|
-
<Stop offset="0.55" stopColor={colors.primary} stopOpacity={0.06} />
|
|
127
|
-
<Stop offset="1" stopColor={colors.primary} stopOpacity={0} />
|
|
128
|
-
</RadialGradient>
|
|
129
|
-
</Defs>
|
|
130
|
-
|
|
131
115
|
{/* ── The document, tucked BEHIND the phone: portrait, chip, MRZ. ── */}
|
|
132
116
|
<G rotation={7} origin="212, 112">
|
|
133
117
|
<Rect
|
|
@@ -224,7 +208,12 @@ export function NfcScanIllustration(): React.ReactElement {
|
|
|
224
208
|
</G>
|
|
225
209
|
|
|
226
210
|
{/* ── The field, over both objects. ── */}
|
|
227
|
-
|
|
211
|
+
{/* The field falls off with distance — the reason the document has to
|
|
212
|
+
be held CLOSE. Flat concentric steps (no gradients in the flow,
|
|
213
|
+
house rule 2026-08-29). */}
|
|
214
|
+
<Circle cx={COUPLING.x} cy={COUPLING.y} r={90} fill={colors.primary} fillOpacity={0.04} />
|
|
215
|
+
<Circle cx={COUPLING.x} cy={COUPLING.y} r={56} fill={colors.primary} fillOpacity={0.07} />
|
|
216
|
+
<Circle cx={COUPLING.x} cy={COUPLING.y} r={28} fill={colors.primary} fillOpacity={0.12} />
|
|
228
217
|
<Circle cx={COUPLING.x} cy={COUPLING.y} r={4.5} fill={colors.primary} />
|
|
229
218
|
{WAVES.map((w, i) => (
|
|
230
219
|
<AnimatedPath
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import * as ImagePicker from 'expo-image-picker';
|
|
3
|
+
|
|
4
|
+
import { isAcceptedAddressPhotoMimeType } from '../config/addressCollection';
|
|
5
|
+
import { IMAGE_MAX_BYTES } from '../config/uploadLimits';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Getting the door photo off the device — the address step's slim sibling of
|
|
9
|
+
// usePoaAttach. Camera or photo library only: the door photo is a PICTURE of a
|
|
10
|
+
// place, so there is no PDF/file case, and each pick closes the source sheet
|
|
11
|
+
// before the picker opens.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/** What the picker handed back, so the screen can preview it. */
|
|
15
|
+
export interface AddressPhotoPick {
|
|
16
|
+
uri: string;
|
|
17
|
+
mimeType: string | undefined;
|
|
18
|
+
name: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type UploadFn = (pick: AddressPhotoPick) => Promise<void>;
|
|
22
|
+
|
|
23
|
+
export function useAddressPhotoAttach(
|
|
24
|
+
upload: UploadFn,
|
|
25
|
+
setError: (message: string) => void,
|
|
26
|
+
): {
|
|
27
|
+
pick: () => void;
|
|
28
|
+
sheetOpen: boolean;
|
|
29
|
+
closeSheet: () => void;
|
|
30
|
+
choosePhoto: () => Promise<void>;
|
|
31
|
+
takePhoto: () => Promise<void>;
|
|
32
|
+
} {
|
|
33
|
+
const [sheetOpen, setSheetOpen] = useState(false);
|
|
34
|
+
const pick = useCallback(() => setSheetOpen(true), []);
|
|
35
|
+
const closeSheet = useCallback(() => setSheetOpen(false), []);
|
|
36
|
+
|
|
37
|
+
const accept = useCallback(
|
|
38
|
+
async (asset: ImagePicker.ImagePickerAsset | undefined, fallbackName: string) => {
|
|
39
|
+
if (!asset) return;
|
|
40
|
+
if (typeof asset.fileSize === 'number' && asset.fileSize > IMAGE_MAX_BYTES) {
|
|
41
|
+
setError('Photo is too large (max 5 MB).');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const mimeType = asset.mimeType ?? 'image/jpeg';
|
|
45
|
+
// The picker's media filter is advisory on some platforms, so what came
|
|
46
|
+
// back is re-checked rather than trusted.
|
|
47
|
+
if (!isAcceptedAddressPhotoMimeType(mimeType)) {
|
|
48
|
+
setError('Please choose a photo (JPEG, PNG or WebP).');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
await upload({ uri: asset.uri, mimeType, name: asset.fileName ?? fallbackName });
|
|
52
|
+
},
|
|
53
|
+
[setError, upload],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const takePhoto = useCallback(async () => {
|
|
57
|
+
setSheetOpen(false);
|
|
58
|
+
const permission = await ImagePicker.requestCameraPermissionsAsync();
|
|
59
|
+
if (!permission.granted) {
|
|
60
|
+
setError('Camera access is needed to photograph the entrance.');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const result = await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], quality: 1 });
|
|
64
|
+
await accept(result.canceled ? undefined : result.assets[0], 'address-photo.jpg');
|
|
65
|
+
}, [accept, setError]);
|
|
66
|
+
|
|
67
|
+
const choosePhoto = useCallback(async () => {
|
|
68
|
+
setSheetOpen(false);
|
|
69
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
70
|
+
mediaTypes: ['images'],
|
|
71
|
+
quality: 1,
|
|
72
|
+
});
|
|
73
|
+
await accept(result.canceled ? undefined : result.assets[0], 'address-photo.jpg');
|
|
74
|
+
}, [accept]);
|
|
75
|
+
|
|
76
|
+
return { pick, sheetOpen, closeSheet, choosePhoto, takePhoto };
|
|
77
|
+
}
|
|
@@ -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,
|
|
@@ -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
|
|
@@ -41,6 +43,34 @@ export interface UploadResponse {
|
|
|
41
43
|
mediaId: string;
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
/** Which side of a document a capture check is about. */
|
|
47
|
+
export type DocumentCaptureSide = 'front' | 'back';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Ask whether an uploaded document photo will be readable, using the same
|
|
51
|
+
* detectors the server later decides the verification with.
|
|
52
|
+
*/
|
|
53
|
+
export interface DocumentCaptureCheckRequest {
|
|
54
|
+
mediaId: string;
|
|
55
|
+
side: DocumentCaptureSide;
|
|
56
|
+
country: string;
|
|
57
|
+
idType: string;
|
|
58
|
+
/** The workflow the submission will carry, when it carries one. */
|
|
59
|
+
workflowId?: string;
|
|
60
|
+
/** The attempt session from /session/start, when the flow has one. */
|
|
61
|
+
sessionId?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* `false` = looked and found nothing (ask for a retake); `true` = fine;
|
|
66
|
+
* `null` = not applicable, or the server could not look (treat as fine).
|
|
67
|
+
*/
|
|
68
|
+
export interface DocumentCaptureCheckResponse {
|
|
69
|
+
side: DocumentCaptureSide;
|
|
70
|
+
face: boolean | null;
|
|
71
|
+
barcode: boolean | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
44
74
|
export interface KeyPersonInvite {
|
|
45
75
|
keyPersonId: string;
|
|
46
76
|
name: string;
|
|
@@ -142,6 +172,80 @@ export interface SdkConfigResponse {
|
|
|
142
172
|
* carries the same lookup as a RISK signal, and the two must not be confused.
|
|
143
173
|
*/
|
|
144
174
|
geoCountry?: string | null;
|
|
175
|
+
/** Whether the platform has a forward-search backend. Gates the whole
|
|
176
|
+
* address-search step; absent means no search screen, never an error. */
|
|
177
|
+
addressSearch?: boolean;
|
|
178
|
+
/** Which backend: 'autocomplete' (Places, as-you-type) or 'basic'
|
|
179
|
+
* (explicit submit). Absent when addressSearch is false. */
|
|
180
|
+
addressSearchMode?: 'autocomplete' | 'basic';
|
|
181
|
+
/**
|
|
182
|
+
* The framed Google-map picker page (OUR hosted /embed/map + a signed APP
|
|
183
|
+
* grant), for a WebView. Absent when the platform holds no Maps key — the
|
|
184
|
+
* built-in OSM picker is the fallback every map failure degrades to.
|
|
185
|
+
*/
|
|
186
|
+
mapsFrameUrl?: string | null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Address search / reverse geocoding ──────────────────────────────────────
|
|
190
|
+
//
|
|
191
|
+
// Every one of these may fail, and every failure degrades to "place the pin by
|
|
192
|
+
// hand" — never to a blocked flow. Shapes mirror the web SDK's services/api.ts.
|
|
193
|
+
|
|
194
|
+
/** The pin's address broken down — what the details sheet displays as rows. */
|
|
195
|
+
export interface AddressParts {
|
|
196
|
+
street?: string | null;
|
|
197
|
+
area?: string | null;
|
|
198
|
+
city?: string | null;
|
|
199
|
+
state?: string | null;
|
|
200
|
+
postcode?: string | null;
|
|
201
|
+
/**
|
|
202
|
+
* ISO-2 of the pin's OWN country, from the geocoder. On the address scope
|
|
203
|
+
* the declared country follows it (lib/country-adoption.ts); it is never a
|
|
204
|
+
* row in the sheet, which shows the country the flow declared.
|
|
205
|
+
*/
|
|
206
|
+
country?: string | null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface AddressReverseResult {
|
|
210
|
+
line: string | null;
|
|
211
|
+
road: string | null;
|
|
212
|
+
parts?: AddressParts | null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** One candidate from the basic (explicit-submit) forward search. */
|
|
216
|
+
export interface AddressSearchHit {
|
|
217
|
+
label: string;
|
|
218
|
+
lat: number;
|
|
219
|
+
lng: number;
|
|
220
|
+
houseNumber: string | null;
|
|
221
|
+
road: string | null;
|
|
222
|
+
/** ISO-2 of the hit's own country (the declaration derives from it). */
|
|
223
|
+
country?: string | null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** One Places autocomplete suggestion. */
|
|
227
|
+
export interface PlaceSuggestion {
|
|
228
|
+
placeId: string;
|
|
229
|
+
mainText: string;
|
|
230
|
+
secondaryText: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** A picked suggestion, resolved to coordinates + structured pieces. */
|
|
234
|
+
export interface ResolvedPlace {
|
|
235
|
+
lat: number;
|
|
236
|
+
lng: number;
|
|
237
|
+
houseNumber: string | null;
|
|
238
|
+
road: string | null;
|
|
239
|
+
formatted: string | null;
|
|
240
|
+
area?: string | null;
|
|
241
|
+
city?: string | null;
|
|
242
|
+
state?: string | null;
|
|
243
|
+
postcode?: string | null;
|
|
244
|
+
/**
|
|
245
|
+
* ISO-2 of the picked address's own country — the declaration derives from
|
|
246
|
+
* it on the address scope (a pick is the applicant's own statement).
|
|
247
|
+
*/
|
|
248
|
+
country?: string | null;
|
|
145
249
|
}
|
|
146
250
|
|
|
147
251
|
/**
|
|
@@ -182,6 +286,20 @@ export interface WorkflowResolutionResponse {
|
|
|
182
286
|
* carries the same lookup as a RISK signal, and the two must not be confused.
|
|
183
287
|
*/
|
|
184
288
|
geoCountry?: string | null;
|
|
289
|
+
/**
|
|
290
|
+
* The address-search availability flags, when the resolution route carries
|
|
291
|
+
* them.
|
|
292
|
+
*
|
|
293
|
+
* A `workflowId` mount skips `/config` entirely, so these are the only route
|
|
294
|
+
* by which such a mount could learn a search backend exists. The server does
|
|
295
|
+
* not serve them here yet (only `/config` and the hosted bootstrap do), which
|
|
296
|
+
* is why they degrade to "no search screen" rather than an error.
|
|
297
|
+
*/
|
|
298
|
+
addressSearch?: boolean;
|
|
299
|
+
addressSearchMode?: 'autocomplete' | 'basic';
|
|
300
|
+
/** The framed Google-map picker page, when the route carries one (see
|
|
301
|
+
* SdkConfigResponse.mapsFrameUrl). */
|
|
302
|
+
mapsFrameUrl?: string | null;
|
|
185
303
|
/** KYB only: the mapped applicant workflow, when configured and resolvable. */
|
|
186
304
|
applicantWorkflow?: ApplicantWorkflowPayload | null;
|
|
187
305
|
}
|
|
@@ -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
|