@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
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import React, { useCallback, useState } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { useKyc, useKycConfig, useKycStore, useTheme } from '../components/runtime';
|
|
4
|
+
import { spacing } from '../config/theme';
|
|
5
|
+
import { useEffectiveCountry, useKyc, useKycConfig, useKycStore, useTheme } from '../components/runtime';
|
|
6
6
|
import { MyazaText } from '../components/Typography';
|
|
7
7
|
import { MyazaButton } from '../components/MyazaButton';
|
|
8
|
-
import { Icon } from '../components/Icon';
|
|
9
8
|
import { MediaSourceSheet } from '../components/MediaSourceSheet';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
9
|
+
import { PoaDocumentTypeList } from './PoaDocumentTypeList';
|
|
10
|
+
import { configScope } from '../lib/scope';
|
|
12
11
|
import { withRetry } from '../services/retry';
|
|
13
12
|
import { compressDocumentImage } from '../services/mediaCompress';
|
|
13
|
+
import { uploadFailureMessage } from '../services/uploadErrors';
|
|
14
14
|
import { poaDocumentTypes, poaMaxAgeDays, poaTypeLabel } from '../config/proofOfAddress';
|
|
15
15
|
import { usePoaAttach, type PoaPick } from './usePoaAttach';
|
|
16
|
+
import { AddressCountryControl, poaOfferedCountries } from './AddressCountryControl';
|
|
17
|
+
import { poaCountryDeclared } from '../lib/poa-country-gate';
|
|
18
|
+
import { PoaDropzone, PoaUploadedRow } from './ProofOfAddressParts';
|
|
16
19
|
import type { PoaDocumentType } from '../types/workflow';
|
|
17
20
|
|
|
18
21
|
// ---------------------------------------------------------------------------
|
|
@@ -28,10 +31,17 @@ import type { PoaDocumentType } from '../types/workflow';
|
|
|
28
31
|
// beyond making sure a file was actually attached.
|
|
29
32
|
// ---------------------------------------------------------------------------
|
|
30
33
|
|
|
31
|
-
export function proofOfAddressMeta(
|
|
34
|
+
export function proofOfAddressMeta(
|
|
35
|
+
maxAgeDays: number,
|
|
36
|
+
/** Whether the workflow's name rule wants the applicant's name on THIS
|
|
37
|
+
* document. False for e.g. a Nigerian utility bill that names the meter,
|
|
38
|
+
* not the tenant — asking for "your name" there sends people hunting for a
|
|
39
|
+
* document they do not have. */
|
|
40
|
+
nameNeeded = true,
|
|
41
|
+
): { title: string; description: string } {
|
|
32
42
|
return {
|
|
33
43
|
title: 'Proof of address',
|
|
34
|
-
description: `Upload a document that shows your name and home address, issued within the last ${maxAgeDays} days.`,
|
|
44
|
+
description: `Upload a document that shows your ${nameNeeded ? 'name and home address' : 'home address'}, issued within the last ${maxAgeDays} days.`,
|
|
35
45
|
};
|
|
36
46
|
}
|
|
37
47
|
|
|
@@ -43,8 +53,23 @@ export function ProofOfAddressStep(): React.ReactElement {
|
|
|
43
53
|
const fileName = useKyc((s) => s.poaFileName);
|
|
44
54
|
const storedType = useKyc((s) => s.poaDocumentType);
|
|
45
55
|
|
|
46
|
-
|
|
56
|
+
// The kinds on offer follow the country — on the address scope the picker
|
|
57
|
+
// below changes it, and an org may accept different documents per market.
|
|
58
|
+
const country = useEffectiveCountry();
|
|
59
|
+
const types = poaDocumentTypes(config.proofOfAddress, country);
|
|
60
|
+
// The flag on the attachment area: on the address scope only a country the
|
|
61
|
+
// applicant picked (the scope has no seeded country to show), else the
|
|
62
|
+
// flow's effective country.
|
|
63
|
+
const selectedCountry = useKyc((s) => s.selectedCountry);
|
|
64
|
+
const flagCountry = configScope(config) === 'address' ? (selectedCountry ?? null) : (country ?? null);
|
|
47
65
|
const [selectedType, setSelectedType] = useState<PoaDocumentType>(storedType ?? types[0]!);
|
|
66
|
+
const typesKey = types.join(',');
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
// A country change can withdraw the picked kind; fall back to the first
|
|
69
|
+
// offered rather than submitting a label that country does not accept.
|
|
70
|
+
if (!types.includes(selectedType)) setSelectedType(types[0]!);
|
|
71
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
72
|
+
}, [typesKey]);
|
|
48
73
|
const [busy, setBusy] = useState(false);
|
|
49
74
|
const [error, setError] = useState<string | null>(null);
|
|
50
75
|
// The picked file, kept for the thumbnail — the only way a user catches
|
|
@@ -67,9 +92,9 @@ export function ProofOfAddressStep(): React.ReactElement {
|
|
|
67
92
|
store.getState().api.upload({ uri: finalUri, type: mimeType, name }, 'proof_of_address'),
|
|
68
93
|
);
|
|
69
94
|
store.getState().setProofOfAddress(id, selectedType, name);
|
|
70
|
-
} catch {
|
|
95
|
+
} catch (e) {
|
|
71
96
|
setPreview(null);
|
|
72
|
-
setError(
|
|
97
|
+
setError(uploadFailureMessage(e));
|
|
73
98
|
} finally {
|
|
74
99
|
setBusy(false);
|
|
75
100
|
}
|
|
@@ -89,6 +114,14 @@ export function ProofOfAddressStep(): React.ReactElement {
|
|
|
89
114
|
}, [store]);
|
|
90
115
|
|
|
91
116
|
const uploaded = Boolean(mediaId) && !busy;
|
|
117
|
+
// The address scope's country is the applicant's declaration and drives the
|
|
118
|
+
// document's market; Continue holds until it is made (the control above
|
|
119
|
+
// asks for it). Never bites elsewhere. See lib/poa-country-gate.ts.
|
|
120
|
+
const countryDeclared = poaCountryDeclared({
|
|
121
|
+
scope: configScope(config),
|
|
122
|
+
selectedCountry,
|
|
123
|
+
offered: poaOfferedCountries(config.proofOfAddress?.countries),
|
|
124
|
+
});
|
|
92
125
|
|
|
93
126
|
return (
|
|
94
127
|
<View>
|
|
@@ -117,111 +150,37 @@ export function ProofOfAddressStep(): React.ReactElement {
|
|
|
117
150
|
},
|
|
118
151
|
]}
|
|
119
152
|
/>
|
|
153
|
+
<AddressCountryControl />
|
|
120
154
|
{types.length > 1 ? (
|
|
121
155
|
<>
|
|
122
156
|
<MyazaText variant="bodySmall" style={{ fontWeight: '600', marginBottom: spacing.xs }}>
|
|
123
157
|
Document type
|
|
124
158
|
</MyazaText>
|
|
125
|
-
<
|
|
159
|
+
<PoaDocumentTypeList
|
|
126
160
|
value={selectedType}
|
|
127
|
-
sheetTitle="Document type"
|
|
128
161
|
// Locked once a file is attached: switching the kind afterwards
|
|
129
162
|
// would mislabel the document already uploaded.
|
|
130
|
-
|
|
163
|
+
disabled={uploaded || busy}
|
|
131
164
|
options={types.map((type) => ({
|
|
132
165
|
value: type,
|
|
133
166
|
label: poaTypeLabel(type, config.proofOfAddress),
|
|
134
167
|
}))}
|
|
135
168
|
onChange={setSelectedType}
|
|
136
169
|
/>
|
|
137
|
-
<View style={{ height: spacing.
|
|
170
|
+
<View style={{ height: spacing.sm }} />
|
|
138
171
|
</>
|
|
139
172
|
) : null}
|
|
140
173
|
|
|
141
174
|
{uploaded ? (
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
borderColor: colors.border,
|
|
150
|
-
backgroundColor: colors.backgroundSecondary,
|
|
151
|
-
}}
|
|
152
|
-
>
|
|
153
|
-
{preview && !preview.isPdf ? (
|
|
154
|
-
<Image
|
|
155
|
-
source={{ uri: preview.uri }}
|
|
156
|
-
style={{ width: 48, height: 48, borderRadius: radius.sm }}
|
|
157
|
-
resizeMode="cover"
|
|
158
|
-
/>
|
|
159
|
-
) : (
|
|
160
|
-
<View
|
|
161
|
-
style={{
|
|
162
|
-
width: 48,
|
|
163
|
-
height: 48,
|
|
164
|
-
borderRadius: radius.sm,
|
|
165
|
-
borderWidth: 1,
|
|
166
|
-
borderColor: colors.border,
|
|
167
|
-
alignItems: 'center',
|
|
168
|
-
justifyContent: 'center',
|
|
169
|
-
}}
|
|
170
|
-
>
|
|
171
|
-
<Icon name="file-text" size={22} color={colors.primary} />
|
|
172
|
-
</View>
|
|
173
|
-
)}
|
|
174
|
-
<View style={{ width: spacing.md, flexShrink: 0 }} />
|
|
175
|
-
<View style={{ flex: 1, minWidth: 0 }}>
|
|
176
|
-
<MyazaText variant="bodySmall" style={{ fontWeight: '600' }} numberOfLines={1}>
|
|
177
|
-
{fileName ?? 'Document uploaded'}
|
|
178
|
-
</MyazaText>
|
|
179
|
-
<MyazaText variant="bodySmall" color={colors.textSecondary} numberOfLines={1}>
|
|
180
|
-
{typeLabel}
|
|
181
|
-
</MyazaText>
|
|
182
|
-
</View>
|
|
183
|
-
<Pressable
|
|
184
|
-
onPress={remove}
|
|
185
|
-
accessibilityRole="button"
|
|
186
|
-
accessibilityLabel="Remove document"
|
|
187
|
-
hitSlop={8}
|
|
188
|
-
>
|
|
189
|
-
<Icon name="x" size={18} color={colors.textSecondary} />
|
|
190
|
-
</Pressable>
|
|
191
|
-
</View>
|
|
175
|
+
<PoaUploadedRow
|
|
176
|
+
preview={preview}
|
|
177
|
+
fileName={fileName}
|
|
178
|
+
typeLabel={typeLabel}
|
|
179
|
+
country={flagCountry}
|
|
180
|
+
onRemove={remove}
|
|
181
|
+
/>
|
|
192
182
|
) : (
|
|
193
|
-
<
|
|
194
|
-
onPress={() => {
|
|
195
|
-
if (!busy) void pick();
|
|
196
|
-
}}
|
|
197
|
-
accessibilityRole="button"
|
|
198
|
-
accessibilityLabel={`Upload your ${typeLabel.toLowerCase()}`}
|
|
199
|
-
style={{
|
|
200
|
-
alignItems: 'center',
|
|
201
|
-
justifyContent: 'center',
|
|
202
|
-
paddingVertical: spacing.xl,
|
|
203
|
-
paddingHorizontal: spacing.lg,
|
|
204
|
-
borderRadius: radius.md,
|
|
205
|
-
}}
|
|
206
|
-
>
|
|
207
|
-
<DashedBorder
|
|
208
|
-
color={colors.border}
|
|
209
|
-
radius={radius.md}
|
|
210
|
-
strokeWidth={1.5}
|
|
211
|
-
/>
|
|
212
|
-
{busy ? (
|
|
213
|
-
<ActivityIndicator size="small" color={colors.primary} />
|
|
214
|
-
) : (
|
|
215
|
-
<Icon name="upload" size={30} color={colors.textSecondary} />
|
|
216
|
-
)}
|
|
217
|
-
<View style={{ height: spacing.sm }} />
|
|
218
|
-
<MyazaText variant="bodySmall" style={{ fontWeight: '600' }}>
|
|
219
|
-
{busy ? 'Uploading…' : `Upload your ${typeLabel.toLowerCase()}`}
|
|
220
|
-
</MyazaText>
|
|
221
|
-
<MyazaText variant="bodySmall" color={colors.textSecondary}>
|
|
222
|
-
Photo or PDF, up to 20MB
|
|
223
|
-
</MyazaText>
|
|
224
|
-
</Pressable>
|
|
183
|
+
<PoaDropzone busy={busy} typeLabel={typeLabel} country={flagCountry} onPress={() => void pick()} />
|
|
225
184
|
)}
|
|
226
185
|
|
|
227
186
|
{error ? (
|
|
@@ -233,7 +192,7 @@ export function ProofOfAddressStep(): React.ReactElement {
|
|
|
233
192
|
<View style={{ height: spacing.lg }} />
|
|
234
193
|
<MyazaButton
|
|
235
194
|
label="Continue"
|
|
236
|
-
disabled={!mediaId || busy}
|
|
195
|
+
disabled={!mediaId || busy || !countryDeclared}
|
|
237
196
|
onPress={() => store.getState().nextStep()}
|
|
238
197
|
/>
|
|
239
198
|
</View>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius } from '../config/theme';
|
|
5
|
+
|
|
6
|
+
/** The terminal screens' 88px circular badge: a tinted disc with a 2px ring,
|
|
7
|
+
* holding the outcome icon. Shared by the submitted and result screens. */
|
|
8
|
+
export function Badge({ bg, border, children }: { bg: string; border: string; children: React.ReactNode }): React.ReactElement {
|
|
9
|
+
return (
|
|
10
|
+
<View
|
|
11
|
+
style={{
|
|
12
|
+
width: 88,
|
|
13
|
+
height: 88,
|
|
14
|
+
borderRadius: radius.full,
|
|
15
|
+
backgroundColor: bg,
|
|
16
|
+
borderWidth: 2,
|
|
17
|
+
borderColor: border,
|
|
18
|
+
alignItems: 'center',
|
|
19
|
+
justifyContent: 'center',
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
</View>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../config/theme';
|
|
5
|
+
import type { KYCError } from '../types/verification';
|
|
6
|
+
import { useTheme } from '../components/runtime';
|
|
7
|
+
import { MyazaText } from '../components/Typography';
|
|
8
|
+
import { MyazaButton } from '../components/MyazaButton';
|
|
9
|
+
import { MyazaAlert } from '../components/MyazaAlert';
|
|
10
|
+
import { Icon } from '../components/Icon';
|
|
11
|
+
import { Badge } from './SubmittedBadge';
|
|
12
|
+
|
|
13
|
+
// The submitted step's error screen (split from SubmittedStep, 200-line
|
|
14
|
+
// rule). `onRetry` is null when the failure is not recoverable in-flow (an
|
|
15
|
+
// exhausted balance, a refused key); the step decides, this only renders.
|
|
16
|
+
|
|
17
|
+
const ERROR_TITLES: Record<string, string> = {
|
|
18
|
+
insufficient_credits: 'Credits Exhausted',
|
|
19
|
+
invalid_api_key: 'Authentication Failed',
|
|
20
|
+
feature_disabled: 'Verification Unavailable',
|
|
21
|
+
upload_failed: 'Upload Failed',
|
|
22
|
+
network_error: 'Connection Failed',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function SubmittedError({
|
|
26
|
+
error,
|
|
27
|
+
onRetry,
|
|
28
|
+
onClose,
|
|
29
|
+
}: {
|
|
30
|
+
error: KYCError;
|
|
31
|
+
onRetry: (() => void) | null;
|
|
32
|
+
onClose: () => void;
|
|
33
|
+
}): React.ReactElement {
|
|
34
|
+
const { colors } = useTheme();
|
|
35
|
+
const title = ERROR_TITLES[error.code] ?? 'Submission Failed';
|
|
36
|
+
return (
|
|
37
|
+
<View style={{ flex: 1, justifyContent: 'space-between' }}>
|
|
38
|
+
<View style={{ alignItems: 'center' }}>
|
|
39
|
+
<View style={{ height: spacing.xl }} />
|
|
40
|
+
<Badge bg={colors.errorBg} border={`${colors.error}4D`}>
|
|
41
|
+
<Icon name="alert" size={44} color={colors.error} />
|
|
42
|
+
</Badge>
|
|
43
|
+
<View style={{ height: spacing.lg }} />
|
|
44
|
+
<MyazaText variant="heading1" style={{ textAlign: 'center' }}>
|
|
45
|
+
{title}
|
|
46
|
+
</MyazaText>
|
|
47
|
+
<View style={{ height: spacing.md }} />
|
|
48
|
+
<View style={{ width: '100%', paddingHorizontal: spacing.md }}>
|
|
49
|
+
<MyazaAlert variant="error" title="What happened" message={error.message} />
|
|
50
|
+
</View>
|
|
51
|
+
</View>
|
|
52
|
+
<View style={{ flexDirection: 'row', gap: spacing.md }}>
|
|
53
|
+
{onRetry ? (
|
|
54
|
+
<View style={{ flex: 1 }}>
|
|
55
|
+
<MyazaButton label="Try Again" variant="outline" leadingIcon="refresh" onPress={onRetry} />
|
|
56
|
+
</View>
|
|
57
|
+
) : null}
|
|
58
|
+
<View style={{ flex: 1 }}>
|
|
59
|
+
<MyazaButton label="Close" onPress={onClose} />
|
|
60
|
+
</View>
|
|
61
|
+
</View>
|
|
62
|
+
</View>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../config/theme';
|
|
5
|
+
import { useKycConfig, useKycStore, useTheme } from '../components/runtime';
|
|
6
|
+
import { MyazaText } from '../components/Typography';
|
|
7
|
+
import { MyazaButton } from '../components/MyazaButton';
|
|
8
|
+
import { Icon } from '../components/Icon';
|
|
9
|
+
import { awaitVerificationOutcome, type VerificationOutcome } from '../lib/result-wait';
|
|
10
|
+
import { describeOutcome, describeWaiting } from '../lib/result-copy';
|
|
11
|
+
import { biometricCopyFor } from '../lib/biometric-copy';
|
|
12
|
+
import { configScope } from '../lib/scope';
|
|
13
|
+
import { Badge } from './SubmittedBadge';
|
|
14
|
+
import { SubmittedWaiting } from './SubmittedWaiting';
|
|
15
|
+
import { StaggerIn } from '../components/StaggerIn';
|
|
16
|
+
|
|
17
|
+
// ─── The result screen (a flow that waits for its verdict) ──────────────────
|
|
18
|
+
//
|
|
19
|
+
// A biometric re-authentication delivered in the flow ('both', the default, or 'app')
|
|
20
|
+
// is answered NOW or it is useless. This screen is mounted from the submitted
|
|
21
|
+
// step's FIRST render and shows one loading screen through the selfie upload,
|
|
22
|
+
// the submission (`verificationId` is null until it lands) and the status
|
|
23
|
+
// poll, then the verdict, firing `onResult` once. The wait has a budget: past
|
|
24
|
+
// it the screen says so and the webhook remains the record, exactly as on
|
|
25
|
+
// every other flow. `showDone` is the `doneButton` option: off when the host
|
|
26
|
+
// app dismisses the flow itself from `onResult`.
|
|
27
|
+
|
|
28
|
+
export function SubmittedResult({
|
|
29
|
+
verificationId,
|
|
30
|
+
retry,
|
|
31
|
+
showDone,
|
|
32
|
+
onClose,
|
|
33
|
+
}: {
|
|
34
|
+
verificationId: string | null;
|
|
35
|
+
retry: { attempt: number; total: number } | null;
|
|
36
|
+
showDone: boolean;
|
|
37
|
+
onClose: () => void;
|
|
38
|
+
}): React.ReactElement {
|
|
39
|
+
const { colors } = useTheme();
|
|
40
|
+
const config = useKycConfig();
|
|
41
|
+
const store = useKycStore();
|
|
42
|
+
const [outcome, setOutcome] = useState<VerificationOutcome | null>(null);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!verificationId) return undefined;
|
|
46
|
+
let alive = true;
|
|
47
|
+
const api = store.getState().api;
|
|
48
|
+
void awaitVerificationOutcome({
|
|
49
|
+
fetchStatus: () => api.status(verificationId).catch(() => null),
|
|
50
|
+
}).then((settled) => {
|
|
51
|
+
if (!alive) return;
|
|
52
|
+
setOutcome(settled);
|
|
53
|
+
if (settled.kind === 'settled') {
|
|
54
|
+
config.onResult?.({
|
|
55
|
+
verificationId,
|
|
56
|
+
status: settled.status,
|
|
57
|
+
reason: settled.reason,
|
|
58
|
+
reasonCode: settled.reasonCode,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return () => {
|
|
63
|
+
alive = false;
|
|
64
|
+
};
|
|
65
|
+
// The wait is tied to the verification, not to the callback identity.
|
|
66
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
67
|
+
}, [verificationId]);
|
|
68
|
+
|
|
69
|
+
// The org's own words for these screens, tokens filled (null = the defaults).
|
|
70
|
+
const words = biometricCopyFor(config);
|
|
71
|
+
if (!outcome) {
|
|
72
|
+
const copy = describeWaiting({ scope: configScope(config), waitsForResult: true, retry, override: words.waiting });
|
|
73
|
+
return <SubmittedWaiting title={copy.title} description={copy.description} retrying={retry != null} />;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const copy = describeOutcome(outcome, words);
|
|
77
|
+
const palette =
|
|
78
|
+
copy.tone === 'success'
|
|
79
|
+
? { bg: colors.successBg, fg: colors.success, icon: 'check' as const }
|
|
80
|
+
: copy.tone === 'error'
|
|
81
|
+
? { bg: colors.errorBg, fg: colors.error, icon: 'alert' as const }
|
|
82
|
+
: { bg: `${colors.primary}14`, fg: colors.primary, icon: 'info' as const };
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<View style={{ flex: 1, justifyContent: 'space-between' }}>
|
|
86
|
+
<View style={{ alignItems: 'center' }}>
|
|
87
|
+
<View style={{ height: spacing.xl }} />
|
|
88
|
+
{/* The verdict lands in the order Flutter's does: badge, title,
|
|
89
|
+
description, then Done (delays 0 / 200 / 320 / 600 ms). */}
|
|
90
|
+
<StaggerIn>
|
|
91
|
+
<Badge bg={palette.bg} border={`${palette.fg}4D`}>
|
|
92
|
+
<Icon name={palette.icon} size={44} color={palette.fg} />
|
|
93
|
+
</Badge>
|
|
94
|
+
</StaggerIn>
|
|
95
|
+
<View style={{ height: spacing.lg }} />
|
|
96
|
+
<StaggerIn delayMs={200}>
|
|
97
|
+
<MyazaText variant="heading1" style={{ textAlign: 'center' }}>
|
|
98
|
+
{copy.title}
|
|
99
|
+
</MyazaText>
|
|
100
|
+
</StaggerIn>
|
|
101
|
+
<View style={{ height: spacing.sm }} />
|
|
102
|
+
<StaggerIn delayMs={320}>
|
|
103
|
+
<MyazaText variant="bodyMedium" style={{ textAlign: 'center', paddingHorizontal: spacing.lg }}>
|
|
104
|
+
{copy.description}
|
|
105
|
+
</MyazaText>
|
|
106
|
+
</StaggerIn>
|
|
107
|
+
</View>
|
|
108
|
+
{showDone ? (
|
|
109
|
+
<StaggerIn delayMs={600}>
|
|
110
|
+
<MyazaButton label="Done" onPress={onClose} />
|
|
111
|
+
</StaggerIn>
|
|
112
|
+
) : (
|
|
113
|
+
<View />
|
|
114
|
+
)}
|
|
115
|
+
</View>
|
|
116
|
+
);
|
|
117
|
+
}
|