@myazahq/kyc-sdk-react-native 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -5
- package/android/build.gradle +35 -3
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +77 -0
- package/app.plugin.js +89 -8
- package/ios/HybridMyazaFaceDetector.swift +16 -3
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
- package/package.json +14 -2
- package/src/MyazaBiometricAuth.tsx +119 -0
- package/src/MyazaKYC.tsx +12 -0
- package/src/components/CountryField.tsx +8 -0
- package/src/components/CountryRegionPicker.tsx +184 -0
- package/src/components/DashedBorder.tsx +55 -0
- package/src/components/DialCodePicker.tsx +53 -67
- package/src/components/DialCodeRow.tsx +102 -0
- package/src/components/FramedMapPicker.tsx +122 -0
- package/src/components/GeoBadge.tsx +34 -0
- package/src/components/Icon.tsx +2 -176
- package/src/components/KycFlow.tsx +60 -32
- package/src/components/LineSkeleton.tsx +100 -0
- package/src/components/MapChrome.tsx +67 -0
- package/src/components/MapPinMarker.tsx +54 -0
- package/src/components/MapPinPicker.tsx +179 -0
- package/src/components/MilestoneTrack.tsx +155 -0
- package/src/components/MultiIdProgress.tsx +107 -0
- package/src/components/MyazaInput.tsx +39 -4
- package/src/components/MyazaSelect.tsx +2 -1
- package/src/components/OptionRow.tsx +22 -3
- package/src/components/OwnershipSlider.tsx +102 -0
- package/src/components/PhoneNumberInput.tsx +45 -4
- package/src/components/PoweredBy.tsx +5 -5
- package/src/components/PresenceBlocks.tsx +153 -0
- package/src/components/StaggerIn.tsx +63 -0
- package/src/components/StepView.tsx +43 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/glass/FloatingSheet.tsx +73 -21
- package/src/components/glass/GlassSheet.tsx +14 -2
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +45 -5
- package/src/config/addressCollection.ts +125 -0
- package/src/config/biometricOptions.ts +100 -0
- package/src/config/business.ts +26 -7
- package/src/config/businessDetailsValidity.ts +52 -0
- package/src/config/businessPrefill.ts +72 -0
- package/src/config/businessSteps.ts +20 -3
- package/src/config/consentStep.ts +19 -0
- package/src/config/keyPeople.ts +111 -7
- package/src/config/keyPeopleCorporate.ts +19 -0
- package/src/config/keyPeopleOwnerTypes.ts +12 -0
- package/src/config/keyPeopleOwners.ts +26 -0
- package/src/config/keyPeoplePrefill.ts +138 -0
- package/src/config/keyPeopleSectionDefs.ts +65 -0
- package/src/config/keyPeopleSections.ts +167 -0
- package/src/config/proofOfAddress.ts +54 -11
- package/src/config/regions.ts +24 -0
- package/src/config/registrationHint.ts +1 -1
- package/src/config/stepOrder.ts +100 -6
- package/src/config/theme.ts +8 -2
- package/src/config/uploadLimits.ts +35 -0
- package/src/config/website.ts +36 -0
- package/src/config/workflowMerge.ts +16 -5
- package/src/emrtd/activeAuth.ts +100 -0
- package/src/emrtd/extras.ts +76 -0
- package/src/emrtd/files.ts +5 -0
- package/src/emrtd/index.ts +1 -0
- package/src/emrtd/read.ts +8 -1
- package/src/emrtd/session.ts +41 -0
- package/src/emrtd/stages.ts +11 -0
- package/src/index.ts +54 -1
- package/src/lib/address-current-location.ts +171 -0
- package/src/lib/address-field-modes.ts +119 -0
- package/src/lib/address-flow.ts +172 -0
- package/src/lib/address-helpers.ts +48 -0
- package/src/lib/address-line.ts +76 -0
- package/src/lib/address-pin-move.ts +97 -0
- package/src/lib/address-step-recovery.ts +63 -0
- package/src/lib/authed-image.ts +78 -0
- package/src/lib/biometric-auth.ts +52 -0
- package/src/lib/biometric-copy.ts +51 -0
- package/src/lib/captureRing.ts +83 -0
- package/src/lib/contact-recovery.ts +44 -0
- package/src/lib/country-adoption.ts +89 -0
- package/src/lib/inferred-country.ts +65 -0
- package/src/lib/livenessLayout.ts +49 -0
- package/src/lib/map-frame.ts +159 -0
- package/src/lib/map-tiles.ts +143 -0
- package/src/lib/multi-id.ts +210 -0
- package/src/lib/poa-country-gate.ts +27 -0
- package/src/lib/resubmit.ts +130 -0
- package/src/lib/result-copy.ts +116 -0
- package/src/lib/result-wait.ts +53 -0
- package/src/lib/review-map-surface.ts +26 -0
- package/src/lib/scope.ts +31 -0
- package/src/lib/screen-corners.ts +50 -0
- package/src/lib/selfie-upload-wait.ts +79 -0
- package/src/lib/step-log.ts +24 -3
- package/src/lib/street-view-fov.ts +42 -0
- package/src/lib/use-multi-id-plan.ts +29 -0
- package/src/lib/webview-available.ts +47 -0
- package/src/liveness/useLiveness.ts +6 -1
- package/src/liveness/useModelReady.ts +65 -0
- package/src/liveness/visionCameraFaceDetector.ts +35 -0
- package/src/presence/background-math.ts +100 -0
- package/src/presence/background-store.ts +82 -0
- package/src/presence/background.ts +165 -0
- package/src/presence/foreground-service.ts +193 -0
- package/src/presence/fs.ts +51 -0
- package/src/presence/geofence.ts +41 -0
- package/src/presence/math.ts +44 -0
- package/src/presence/post.ts +40 -0
- package/src/presence/report.ts +87 -0
- package/src/presence/sampler.ts +110 -0
- package/src/presence/status.ts +92 -0
- package/src/presence/store.ts +94 -0
- package/src/presence/tier.ts +36 -0
- package/src/presence/watch-wait.ts +81 -0
- package/src/screens/AddressCountryControl.tsx +115 -0
- package/src/screens/ApplicantRoleStep.tsx +9 -5
- package/src/screens/BusinessCheckPanel.tsx +89 -0
- package/src/screens/BusinessDetailsFields.tsx +112 -0
- package/src/screens/BusinessDetailsStep.tsx +133 -145
- package/src/screens/BusinessDocumentSlot.tsx +5 -5
- package/src/screens/BusinessDocumentsStep.tsx +5 -56
- package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
- package/src/screens/BusinessPickedCard.tsx +86 -0
- package/src/screens/BusinessPickedSection.tsx +51 -0
- package/src/screens/BusinessRegistryPickers.tsx +67 -0
- package/src/screens/BusinessSandboxToggle.tsx +114 -0
- package/src/screens/BusinessSearch.tsx +199 -0
- package/src/screens/BusinessSearchResults.tsx +115 -0
- package/src/screens/CompanyInfoFields.tsx +91 -51
- package/src/screens/ConsentStep.tsx +37 -17
- package/src/screens/ContactActions.tsx +54 -0
- package/src/screens/ContactDestinationField.tsx +5 -0
- package/src/screens/ContactEntryPanel.tsx +75 -0
- package/src/screens/ContactVerificationStep.tsx +43 -42
- package/src/screens/CountrySelectStep.tsx +13 -147
- package/src/screens/IdTypeStep.tsx +12 -5
- package/src/screens/KeepLinksSheet.tsx +88 -0
- package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
- package/src/screens/KeyPeopleAwaitList.tsx +126 -0
- package/src/screens/KeyPeoplePending.tsx +154 -0
- package/src/screens/KeyPeopleSection.tsx +162 -0
- package/src/screens/KeyPeopleSectionsList.tsx +103 -0
- package/src/screens/KeyPeopleUboExemption.tsx +72 -0
- package/src/screens/KeyPersonCard.tsx +36 -4
- package/src/screens/KeyPersonForm.tsx +195 -36
- package/src/screens/KeyPersonKindToggle.tsx +68 -0
- package/src/screens/KeyPersonOwners.tsx +109 -0
- package/src/screens/KeyPersonRoleChips.tsx +68 -0
- package/src/screens/KeyPersonSheet.tsx +92 -108
- package/src/screens/LivenessAvatar.tsx +15 -3
- package/src/screens/LivenessStep.tsx +172 -52
- package/src/screens/NfcStep.tsx +14 -0
- package/src/screens/PoaDocumentTypeList.tsx +66 -0
- package/src/screens/ProofOfAddressParts.tsx +151 -0
- package/src/screens/ProofOfAddressStep.tsx +58 -96
- package/src/screens/SubmittedBadge.tsx +25 -0
- package/src/screens/SubmittedError.tsx +64 -0
- package/src/screens/SubmittedResult.tsx +117 -0
- package/src/screens/SubmittedStep.tsx +82 -119
- package/src/screens/SubmittedSuccess.tsx +127 -0
- package/src/screens/SubmittedWaiting.tsx +45 -0
- package/src/screens/address/AddressEntranceStep.tsx +161 -0
- package/src/screens/address/AddressIntroGate.tsx +148 -0
- package/src/screens/address/AddressMapStub.tsx +50 -0
- package/src/screens/address/AddressPinStep.tsx +178 -0
- package/src/screens/address/AddressReviewStep.tsx +162 -0
- package/src/screens/address/AddressSandboxTabs.tsx +158 -0
- package/src/screens/address/AddressSearchField.tsx +115 -0
- package/src/screens/address/AddressSearchStep.tsx +94 -0
- package/src/screens/address/CurrentLocationRow.tsx +135 -0
- package/src/screens/address/DetailsSheet.tsx +175 -0
- package/src/screens/address/DetailsSheetFields.tsx +184 -0
- package/src/screens/address/EntranceDropzone.tsx +178 -0
- package/src/screens/address/EntranceFraming.tsx +66 -0
- package/src/screens/address/EntrancePills.tsx +60 -0
- package/src/screens/address/FramedStreetView.tsx +155 -0
- package/src/screens/address/IntroDisclosures.tsx +210 -0
- package/src/screens/address/LabelDecisionRow.tsx +113 -0
- package/src/screens/address/PinSummaryRow.tsx +105 -0
- package/src/screens/address/ReviewAddressBand.tsx +111 -0
- package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
- package/src/screens/address/ReviewMapPicture.tsx +106 -0
- package/src/screens/address/SearchResults.tsx +105 -0
- package/src/screens/address/SearchScreen.tsx +176 -0
- package/src/screens/address/SkipForNow.tsx +43 -0
- package/src/screens/address/StreetViewChrome.tsx +81 -0
- package/src/screens/address/detail-values.ts +22 -0
- package/src/screens/address/fix-source.ts +27 -0
- package/src/screens/address/index.ts +8 -0
- package/src/screens/address/meta.ts +44 -0
- package/src/screens/address/use-address-flow.ts +200 -0
- package/src/screens/address/use-label-pin.ts +80 -0
- package/src/screens/address/use-pin-actions.ts +192 -0
- package/src/screens/biometric/BiometricAuthFlow.tsx +170 -0
- package/src/screens/consent/model.ts +79 -10
- package/src/screens/liveness/CaptureRing.tsx +91 -0
- package/src/screens/liveness/LivenessCamera.tsx +63 -0
- package/src/screens/liveness/LivenessHandover.tsx +43 -0
- package/src/screens/liveness/LivenessOutcome.tsx +4 -3
- package/src/screens/liveness/SelfiePreview.tsx +13 -4
- package/src/screens/liveness/ShutterFlash.tsx +25 -0
- package/src/screens/liveness/index.ts +4 -0
- package/src/screens/liveness/useSelfieUpload.ts +23 -3
- package/src/screens/nfc/NfcReadProgress.tsx +1 -0
- package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
- package/src/screens/useAddressPhotoAttach.ts +77 -0
- package/src/screens/useAwaitingPeople.ts +90 -0
- package/src/screens/useBusinessDocumentAttach.ts +93 -0
- package/src/screens/usePoaAttach.ts +12 -15
- package/src/services/api-biometric.ts +37 -0
- package/src/services/api-types-biometric.ts +41 -0
- package/src/services/api-types.ts +251 -3
- package/src/services/api-verify-types.ts +90 -1
- package/src/services/api.ts +237 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/errors.ts +10 -2
- package/src/services/location.ts +234 -0
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +8 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
- package/src/store/address.ts +97 -0
- package/src/store/businessCheck.ts +113 -0
- package/src/store/derive.ts +44 -4
- package/src/store/kycStore.ts +352 -16
- package/src/store/serverConfig.ts +20 -0
- package/src/store/session.ts +227 -0
- package/src/store/state.ts +289 -2
- package/src/store/submit.ts +80 -13
- package/src/store/submitApplicant.ts +5 -11
- package/src/types/business.ts +23 -2
- package/src/types/config.ts +70 -2
- package/src/types/verification.ts +13 -1
- package/src/types/workflow.ts +112 -1
- package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../config/theme';
|
|
5
|
+
import { MyazaAlert } from '../components/MyazaAlert';
|
|
6
|
+
import { ContactDestinationField } from './ContactDestinationField';
|
|
7
|
+
import { ContactChannelChoice } from './ContactChannelChoice';
|
|
8
|
+
import type { PhoneOtpChannel } from '../config/contact';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// The first half of the contact step: where to send the code, and (when the
|
|
12
|
+
// workflow offers a choice) how. Composed as one widget so the step has a
|
|
13
|
+
// single child per phase. Split from ContactVerificationStep (200-line rule);
|
|
14
|
+
// mirrors Flutter's ContactEntryPanel.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export function ContactEntryPanel({
|
|
18
|
+
isEmail,
|
|
19
|
+
recovery,
|
|
20
|
+
email,
|
|
21
|
+
onEmailChange,
|
|
22
|
+
onPhoneChange,
|
|
23
|
+
defaultCountry,
|
|
24
|
+
geoCountry,
|
|
25
|
+
error,
|
|
26
|
+
disabled,
|
|
27
|
+
offeredChannels,
|
|
28
|
+
via,
|
|
29
|
+
onPickChannel,
|
|
30
|
+
}: {
|
|
31
|
+
isEmail: boolean;
|
|
32
|
+
/** Entered via submit recovery: the server refused this channel's proof. */
|
|
33
|
+
recovery: boolean;
|
|
34
|
+
email: string;
|
|
35
|
+
onEmailChange: (value: string) => void;
|
|
36
|
+
onPhoneChange: (value: { e164: string; isValid: boolean }) => void;
|
|
37
|
+
defaultCountry?: string;
|
|
38
|
+
geoCountry?: string | null;
|
|
39
|
+
error: string | null;
|
|
40
|
+
disabled: boolean;
|
|
41
|
+
offeredChannels: PhoneOtpChannel[];
|
|
42
|
+
via: PhoneOtpChannel;
|
|
43
|
+
onPickChannel: (channel: PhoneOtpChannel) => void;
|
|
44
|
+
}): React.ReactElement {
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
{recovery && (
|
|
48
|
+
<>
|
|
49
|
+
<MyazaAlert
|
|
50
|
+
variant="warning"
|
|
51
|
+
title="Please verify again"
|
|
52
|
+
message={`Your earlier confirmation has expired, so please verify ${isEmail ? 'your email' : 'your number'} once more. Everything else is saved, and we will submit again straight after.`}
|
|
53
|
+
/>
|
|
54
|
+
<View style={{ height: spacing.md }} />
|
|
55
|
+
</>
|
|
56
|
+
)}
|
|
57
|
+
<ContactDestinationField
|
|
58
|
+
isEmail={isEmail}
|
|
59
|
+
email={email}
|
|
60
|
+
onEmailChange={onEmailChange}
|
|
61
|
+
onPhoneChange={onPhoneChange}
|
|
62
|
+
defaultCountry={defaultCountry}
|
|
63
|
+
geoCountry={geoCountry}
|
|
64
|
+
error={error}
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
/>
|
|
67
|
+
<ContactChannelChoice
|
|
68
|
+
offered={offeredChannels}
|
|
69
|
+
picked={via}
|
|
70
|
+
disabled={disabled}
|
|
71
|
+
onPick={onPickChannel}
|
|
72
|
+
/>
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { View } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { spacing } from '../config/theme';
|
|
5
5
|
import { useEffectiveCountry, useKyc, useKycConfig, useKycStore } from '../components/runtime';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { ContactActions } from './ContactActions';
|
|
7
|
+
import { ContactEntryPanel } from './ContactEntryPanel';
|
|
8
8
|
import { ContactVerifiedPanel } from './ContactVerifiedPanel';
|
|
9
9
|
import { ContactCodeStep } from './ContactCodeStep';
|
|
10
|
-
import { ContactChannelChoice } from './ContactChannelChoice';
|
|
11
|
-
import { ContactFooterNote } from './ContactFooterNote';
|
|
12
10
|
import {
|
|
13
11
|
contactCodeLength,
|
|
14
12
|
contactIsRequired,
|
|
@@ -17,6 +15,7 @@ import {
|
|
|
17
15
|
type PhoneOtpChannel,
|
|
18
16
|
} from '../config/contact';
|
|
19
17
|
import { attemptsRemaining, describeCheckError, describeSendError } from '../services/contactErrors';
|
|
18
|
+
import { stepAfterContactVerified } from '../lib/contact-recovery';
|
|
20
19
|
|
|
21
20
|
// ---------------------------------------------------------------------------
|
|
22
21
|
// Contact-verification OTP step — email or phone, one component, two mounts.
|
|
@@ -37,6 +36,7 @@ export function ContactVerificationStep({
|
|
|
37
36
|
const country = useEffectiveCountry();
|
|
38
37
|
const store = useKycStore();
|
|
39
38
|
const contact = useKyc((s) => s.contact);
|
|
39
|
+
const serverConfig = useKyc((s) => s.serverConfig);
|
|
40
40
|
|
|
41
41
|
const isEmail = channel === 'email';
|
|
42
42
|
const stepConfig = isEmail ? config.emailVerification : config.phoneVerification;
|
|
@@ -54,10 +54,9 @@ export function ContactVerificationStep({
|
|
|
54
54
|
const [destination, setDestination] = useState(contact.emailAddress ?? '');
|
|
55
55
|
// The phone field emits E.164 + validity from libphonenumber, so the step no
|
|
56
56
|
// longer guesses either from raw text.
|
|
57
|
-
const [phone, setPhone] = useState<{ e164: string; isValid: boolean }>(
|
|
58
|
-
e164: contact.phoneNumber ?? '',
|
|
59
|
-
|
|
60
|
-
});
|
|
57
|
+
const [phone, setPhone] = useState<{ e164: string; isValid: boolean }>(
|
|
58
|
+
{ e164: contact.phoneNumber ?? '', isValid: false },
|
|
59
|
+
);
|
|
61
60
|
const [challengeId, setChallengeId] = useState<string | null>(null);
|
|
62
61
|
const [expiresAt, setExpiresAt] = useState<string | null>(null);
|
|
63
62
|
const [code, setCode] = useState('');
|
|
@@ -69,7 +68,20 @@ export function ContactVerificationStep({
|
|
|
69
68
|
const value = isEmail ? destination.trim() : phone.e164;
|
|
70
69
|
const canSend = isEmail ? isValidContactEmail(value) : phone.isValid;
|
|
71
70
|
|
|
72
|
-
|
|
71
|
+
// Entered via submit recovery? (The server refused this channel's proof at
|
|
72
|
+
// submit — the token had expired or was already claimed.) Captured at mount:
|
|
73
|
+
// verifying clears the flag, and Continue must still route back to
|
|
74
|
+
// 'submitted' afterwards (which auto-submits with the fresh proof).
|
|
75
|
+
const recovery = useRef((contact.expired ?? []).includes(channel)).current;
|
|
76
|
+
const advance = useCallback(() => {
|
|
77
|
+
const target = stepAfterContactVerified({
|
|
78
|
+
recovery,
|
|
79
|
+
expired: store.getState().contact.expired ?? [],
|
|
80
|
+
channel,
|
|
81
|
+
});
|
|
82
|
+
if (target) store.getState().goToStep(target);
|
|
83
|
+
else store.getState().nextStep();
|
|
84
|
+
}, [store, channel, recovery]);
|
|
73
85
|
|
|
74
86
|
// The sheet header is rendered by the shell and cannot see this step's state,
|
|
75
87
|
// so publish what it needs to caption: the channel in play now, and the
|
|
@@ -158,42 +170,31 @@ export function ContactVerificationStep({
|
|
|
158
170
|
onResend={(switchTo) => void send(switchTo)}
|
|
159
171
|
/>
|
|
160
172
|
) : (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
onPick={setVia}
|
|
176
|
-
/>
|
|
177
|
-
</>
|
|
173
|
+
<ContactEntryPanel
|
|
174
|
+
isEmail={isEmail}
|
|
175
|
+
recovery={recovery}
|
|
176
|
+
email={destination}
|
|
177
|
+
onEmailChange={setDestination}
|
|
178
|
+
onPhoneChange={setPhone}
|
|
179
|
+
defaultCountry={config.phoneVerification?.defaultCountry ?? country ?? serverConfig.geoCountry ?? undefined}
|
|
180
|
+
geoCountry={serverConfig.geoCountry}
|
|
181
|
+
error={error}
|
|
182
|
+
disabled={busy}
|
|
183
|
+
offeredChannels={offeredChannels}
|
|
184
|
+
via={via}
|
|
185
|
+
onPickChannel={setVia}
|
|
186
|
+
/>
|
|
178
187
|
)}
|
|
179
188
|
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
loading={busy}
|
|
189
|
+
<ContactActions
|
|
190
|
+
hasChallenge={challengeId != null}
|
|
191
|
+
busy={busy}
|
|
184
192
|
disabled={challengeId ? code.length !== codeLength : !canSend}
|
|
185
|
-
|
|
193
|
+
required={required}
|
|
194
|
+
isEmail={isEmail}
|
|
195
|
+
onPrimary={() => (challengeId ? void check(code) : void send())}
|
|
196
|
+
onSkip={advance}
|
|
186
197
|
/>
|
|
187
|
-
|
|
188
|
-
{!required ? (
|
|
189
|
-
<>
|
|
190
|
-
<View style={{ height: spacing.sm }} />
|
|
191
|
-
<MyazaButton label="Skip for now" variant="ghost" disabled={busy} onPress={advance} />
|
|
192
|
-
</>
|
|
193
|
-
) : null}
|
|
194
|
-
|
|
195
|
-
<View style={{ height: spacing.md }} />
|
|
196
|
-
<ContactFooterNote isEmail={isEmail} />
|
|
197
198
|
</View>
|
|
198
199
|
);
|
|
199
200
|
}
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import React, { useMemo
|
|
2
|
-
import {
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { MyazaInput } from '../components/MyazaInput';
|
|
8
|
-
import { CountryFlag } from '../components/CountryFlag';
|
|
9
|
-
import { Icon } from '../components/Icon';
|
|
10
|
-
import { groupCountriesByRegion, regionCountryName } from '../config/regions';
|
|
4
|
+
import { useKyc, useKycConfig, useKycStore } from '../components/runtime';
|
|
5
|
+
import { CountryRegionPicker, CountryRow } from '../components/CountryRegionPicker';
|
|
6
|
+
import { regionCountryName } from '../config/regions';
|
|
11
7
|
import { countrySelectOptions } from '../store/derive';
|
|
12
8
|
|
|
13
9
|
// ---------------------------------------------------------------------------
|
|
@@ -54,7 +50,14 @@ export function CountrySelectStep(): React.ReactElement {
|
|
|
54
50
|
};
|
|
55
51
|
|
|
56
52
|
if (options.length > COUNTRY_SEARCH_THRESHOLD) {
|
|
57
|
-
return
|
|
53
|
+
return (
|
|
54
|
+
<CountryRegionPicker
|
|
55
|
+
countries={options}
|
|
56
|
+
selected={selected}
|
|
57
|
+
geoCountry={serverConfig.geoCountry}
|
|
58
|
+
onPick={pick}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
return (
|
|
@@ -71,140 +74,3 @@ export function CountrySelectStep(): React.ReactElement {
|
|
|
71
74
|
</View>
|
|
72
75
|
);
|
|
73
76
|
}
|
|
74
|
-
|
|
75
|
-
function CountryRegionPicker({
|
|
76
|
-
countries,
|
|
77
|
-
selected,
|
|
78
|
-
onPick,
|
|
79
|
-
}: {
|
|
80
|
-
countries: string[];
|
|
81
|
-
selected: string | null;
|
|
82
|
-
onPick: (country: string) => void;
|
|
83
|
-
}): React.ReactElement {
|
|
84
|
-
const { colors } = useTheme();
|
|
85
|
-
const [query, setQuery] = useState('');
|
|
86
|
-
|
|
87
|
-
const groups = useMemo(() => {
|
|
88
|
-
const needle = query.trim().toLowerCase();
|
|
89
|
-
const filtered = needle
|
|
90
|
-
? countries.filter(
|
|
91
|
-
(code) =>
|
|
92
|
-
code.toLowerCase().includes(needle) ||
|
|
93
|
-
regionCountryName(code).toLowerCase().includes(needle),
|
|
94
|
-
)
|
|
95
|
-
: countries;
|
|
96
|
-
return groupCountriesByRegion(filtered);
|
|
97
|
-
}, [countries, query]);
|
|
98
|
-
|
|
99
|
-
const empty = groups.length === 0;
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
// The search box stays put and the list owns its own scroll, so a
|
|
103
|
-
// ~240-country flow does not scroll the search field off the top. That
|
|
104
|
-
// relies on the SHEET rendering this step with `fillsViewport` — inside a
|
|
105
|
-
// scroll view the nested list below would take an unbounded height, outgrow
|
|
106
|
-
// the viewport, and drag the search box off the top with it.
|
|
107
|
-
<View style={{ flex: 1 }}>
|
|
108
|
-
<MyazaInput
|
|
109
|
-
value={query}
|
|
110
|
-
onChangeText={setQuery}
|
|
111
|
-
placeholder="Search countries…"
|
|
112
|
-
autoCapitalize="none"
|
|
113
|
-
prefix={<Icon name="search" size={18} color={colors.textSecondary} />}
|
|
114
|
-
/>
|
|
115
|
-
<View style={{ height: spacing.md }} />
|
|
116
|
-
|
|
117
|
-
{empty ? (
|
|
118
|
-
<MyazaText variant="bodyMedium" style={{ textAlign: 'center', marginTop: spacing.lg }}>
|
|
119
|
-
No countries match your search.
|
|
120
|
-
</MyazaText>
|
|
121
|
-
) : (
|
|
122
|
-
// `nestedScrollEnabled` stays: Android refuses to scroll a nested
|
|
123
|
-
// same-direction list without it, and this is still nested on the
|
|
124
|
-
// flat-list path and inside any future scrolling parent.
|
|
125
|
-
<ScrollView
|
|
126
|
-
nestedScrollEnabled
|
|
127
|
-
keyboardShouldPersistTaps="handled"
|
|
128
|
-
showsVerticalScrollIndicator={false}
|
|
129
|
-
>
|
|
130
|
-
{groups.map((group) => (
|
|
131
|
-
<View key={group.region}>
|
|
132
|
-
{/* Uppercase, tracked-out section label — Flutter's region
|
|
133
|
-
header. Lowercase text at the default weight read as another
|
|
134
|
-
list row rather than a divider. */}
|
|
135
|
-
<MyazaText
|
|
136
|
-
variant="bodySmall"
|
|
137
|
-
color={colors.textSecondary}
|
|
138
|
-
style={{
|
|
139
|
-
fontWeight: '700',
|
|
140
|
-
letterSpacing: 0.6,
|
|
141
|
-
paddingHorizontal: spacing.xs,
|
|
142
|
-
paddingVertical: spacing.sm,
|
|
143
|
-
}}
|
|
144
|
-
>
|
|
145
|
-
{group.region.toUpperCase()}
|
|
146
|
-
</MyazaText>
|
|
147
|
-
{group.countries.map((entry) => (
|
|
148
|
-
<CountryRow
|
|
149
|
-
key={entry.code}
|
|
150
|
-
code={entry.code}
|
|
151
|
-
name={entry.name}
|
|
152
|
-
selected={entry.code === selected}
|
|
153
|
-
onPress={() => onPick(entry.code)}
|
|
154
|
-
/>
|
|
155
|
-
))}
|
|
156
|
-
</View>
|
|
157
|
-
))}
|
|
158
|
-
</ScrollView>
|
|
159
|
-
)}
|
|
160
|
-
</View>
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function CountryRow({
|
|
165
|
-
code,
|
|
166
|
-
name,
|
|
167
|
-
selected,
|
|
168
|
-
onPress,
|
|
169
|
-
}: {
|
|
170
|
-
code: string;
|
|
171
|
-
name: string;
|
|
172
|
-
selected: boolean;
|
|
173
|
-
onPress: () => void;
|
|
174
|
-
}): React.ReactElement {
|
|
175
|
-
const { colors } = useTheme();
|
|
176
|
-
return (
|
|
177
|
-
<Pressable
|
|
178
|
-
onPress={onPress}
|
|
179
|
-
accessibilityRole="radio"
|
|
180
|
-
accessibilityState={{ checked: selected }}
|
|
181
|
-
accessibilityLabel={name}
|
|
182
|
-
// Matches the Flutter SDK's CountryOptionTile exactly — the two had
|
|
183
|
-
// drifted (24px flag / 12px padding here vs 32 / 16 there), which is why
|
|
184
|
-
// the same screen looked like a different component on each platform. The
|
|
185
|
-
// spacing and radius scales are identical across the SDKs, so there is
|
|
186
|
-
// nothing platform-specific to reconcile: `md` means 16 on both.
|
|
187
|
-
style={{
|
|
188
|
-
flexDirection: 'row',
|
|
189
|
-
alignItems: 'center',
|
|
190
|
-
padding: spacing.md,
|
|
191
|
-
borderRadius: radius.md,
|
|
192
|
-
borderWidth: selected ? 1.5 : 1,
|
|
193
|
-
borderColor: selected ? colors.primary : colors.border,
|
|
194
|
-
backgroundColor: selected ? colors.primary50 : colors.background,
|
|
195
|
-
marginBottom: spacing.sm,
|
|
196
|
-
}}
|
|
197
|
-
>
|
|
198
|
-
<CountryFlag country={code} size={32} />
|
|
199
|
-
<View style={{ width: 12 }} />
|
|
200
|
-
<MyazaText variant="label" style={{ flex: 1, fontWeight: '500' }}>
|
|
201
|
-
{name}
|
|
202
|
-
</MyazaText>
|
|
203
|
-
<Icon
|
|
204
|
-
name="chevron-right"
|
|
205
|
-
size={18}
|
|
206
|
-
color={selected ? colors.primary : colors.textSecondary}
|
|
207
|
-
/>
|
|
208
|
-
</Pressable>
|
|
209
|
-
);
|
|
210
|
-
}
|
|
@@ -3,6 +3,7 @@ import { Pressable, View } from 'react-native';
|
|
|
3
3
|
|
|
4
4
|
import { radius, spacing } from '../config/theme';
|
|
5
5
|
import { ID_TYPES, resolveIdTypeDefinition } from '../config/idTypes';
|
|
6
|
+
import { useMultiIdPlan } from '../lib/use-multi-id-plan';
|
|
6
7
|
import type { IdType, IdTypeDefinition } from '../types/config';
|
|
7
8
|
import {
|
|
8
9
|
useEffectiveCountry,
|
|
@@ -54,6 +55,10 @@ export function IdTypeStep(): React.ReactElement {
|
|
|
54
55
|
const store = useKycStore();
|
|
55
56
|
const serverConfig = useKyc((s) => s.serverConfig);
|
|
56
57
|
const selectedIdType = useKyc((s) => s.selectedIdType);
|
|
58
|
+
// Multi-ID: this picker is for ONE check of several. It may only offer picks
|
|
59
|
+
// that leave every later check something to offer — the server validates the
|
|
60
|
+
// same rule, so an unsafe pick would be a submission it rejects.
|
|
61
|
+
const plan = useMultiIdPlan();
|
|
57
62
|
|
|
58
63
|
// Select AND advance in one tap. Both go through the store so `nextStep`
|
|
59
64
|
// routes off the value just written — a document ID to document-capture, a
|
|
@@ -104,13 +109,15 @@ export function IdTypeStep(): React.ReactElement {
|
|
|
104
109
|
// what makes the disabled step actually disappear from the live flow rather
|
|
105
110
|
// than still offering passports and licences that then have nowhere to be
|
|
106
111
|
// captured. Mirrors the web SDK's IdTypeStep.
|
|
107
|
-
const visible = useMemo<IdTypeDefinition[]>(
|
|
108
|
-
|
|
112
|
+
const visible = useMemo<IdTypeDefinition[]>(() => {
|
|
113
|
+
const base =
|
|
109
114
|
config.enableDocumentCapture === false
|
|
110
115
|
? available.filter((t) => !t.requiresDocumentCapture)
|
|
111
|
-
: available
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
: available;
|
|
117
|
+
// Multi-ID: only the picks that are still free AND leave every later check
|
|
118
|
+
// an option. An ID already used earlier in the run is simply not offered.
|
|
119
|
+
return plan ? base.filter((t) => plan.safeOptions.includes(t.key)) : base;
|
|
120
|
+
}, [available, config.enableDocumentCapture, plan]);
|
|
114
121
|
|
|
115
122
|
if (serverConfig.status === 'loading') {
|
|
116
123
|
return (
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Linking, View } from 'react-native';
|
|
3
|
+
import * as Clipboard from 'expo-clipboard';
|
|
4
|
+
|
|
5
|
+
import { spacing } from '../config/theme';
|
|
6
|
+
import { FloatingSheet } from '../components/glass/FloatingSheet';
|
|
7
|
+
import { Icon } from '../components/Icon';
|
|
8
|
+
import { MyazaAlert } from '../components/MyazaAlert';
|
|
9
|
+
import { MyazaButton } from '../components/MyazaButton';
|
|
10
|
+
import { MyazaText } from '../components/Typography';
|
|
11
|
+
import { useTheme } from '../components/runtime';
|
|
12
|
+
|
|
13
|
+
// Shown when the applicant taps Done while key people still owe a check.
|
|
14
|
+
//
|
|
15
|
+
// The invite links live on THIS success screen, and on mobile the screen dies
|
|
16
|
+
// with the app: once closed, the applicant has no way back to the links unless
|
|
17
|
+
// the org re-sends them. The session's own hosted web page is the way back —
|
|
18
|
+
// the same rehydrated success screen, alive for as long as the invites are —
|
|
19
|
+
// so the one moment to hand it over is the moment they leave.
|
|
20
|
+
|
|
21
|
+
export function KeepLinksSheet({
|
|
22
|
+
open,
|
|
23
|
+
url,
|
|
24
|
+
onClose,
|
|
25
|
+
onDone,
|
|
26
|
+
}: {
|
|
27
|
+
open: boolean;
|
|
28
|
+
url: string;
|
|
29
|
+
/** Dismiss the sheet only (they are staying on the success screen). */
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
/** Close the sheet AND the flow (they are done here). */
|
|
32
|
+
onDone: () => void;
|
|
33
|
+
}): React.ReactElement {
|
|
34
|
+
const { colors } = useTheme();
|
|
35
|
+
const [copied, setCopied] = useState(false);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<FloatingSheet visible={open} onClose={onClose}>
|
|
39
|
+
<View style={{ padding: spacing.md, paddingTop: spacing.xs, alignItems: 'center' }}>
|
|
40
|
+
<View
|
|
41
|
+
style={{
|
|
42
|
+
width: 52,
|
|
43
|
+
height: 52,
|
|
44
|
+
borderRadius: 26,
|
|
45
|
+
backgroundColor: colors.primary50,
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
justifyContent: 'center',
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<Icon name="globe" size={26} color={colors.primary} />
|
|
51
|
+
</View>
|
|
52
|
+
|
|
53
|
+
<View style={{ height: spacing.md }} />
|
|
54
|
+
<MyazaText variant="heading3" style={{ textAlign: 'center' }}>
|
|
55
|
+
Keep a way back to these links
|
|
56
|
+
</MyazaText>
|
|
57
|
+
<View style={{ height: spacing.md }} />
|
|
58
|
+
<View style={{ width: '100%' }}>
|
|
59
|
+
<MyazaAlert
|
|
60
|
+
variant="warning"
|
|
61
|
+
title="This screen will not be shown again"
|
|
62
|
+
message="Your key people's links live on a web page made for you. Open it in your browser or copy the address somewhere safe, then check who has finished and share the links from there at any time."
|
|
63
|
+
/>
|
|
64
|
+
</View>
|
|
65
|
+
|
|
66
|
+
<View style={{ height: spacing.lg }} />
|
|
67
|
+
<View style={{ width: '100%', gap: spacing.sm }}>
|
|
68
|
+
<MyazaButton
|
|
69
|
+
label="Open in my browser"
|
|
70
|
+
leadingIcon="globe"
|
|
71
|
+
onPress={() => void Linking.openURL(url).catch(() => undefined)}
|
|
72
|
+
/>
|
|
73
|
+
<MyazaButton
|
|
74
|
+
label={copied ? 'Link copied' : 'Copy the page link'}
|
|
75
|
+
variant="outline"
|
|
76
|
+
leadingIcon={copied ? 'check' : 'copy'}
|
|
77
|
+
onPress={() => {
|
|
78
|
+
void Clipboard.setStringAsync(url)
|
|
79
|
+
.then(() => setCopied(true))
|
|
80
|
+
.catch(() => undefined);
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
<MyazaButton label="I'm done here" variant="ghost" onPress={onDone} />
|
|
84
|
+
</View>
|
|
85
|
+
</View>
|
|
86
|
+
</FloatingSheet>
|
|
87
|
+
);
|
|
88
|
+
}
|