@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,199 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { MyazaAlert } from '../components/MyazaAlert';
|
|
5
|
+
import { MyazaButton } from '../components/MyazaButton';
|
|
6
|
+
import { MyazaInput } from '../components/MyazaInput';
|
|
7
|
+
import { MyazaSelect } from '../components/MyazaSelect';
|
|
8
|
+
import { MyazaText } from '../components/Typography';
|
|
9
|
+
import { Icon } from '../components/Icon';
|
|
10
|
+
import { BusinessSearchResults } from './BusinessSearchResults';
|
|
11
|
+
import { useKyc, useKycStore, useTheme } from '../components/runtime';
|
|
12
|
+
import { spacing } from '../config/theme';
|
|
13
|
+
import type { BusinessSearchHit } from '../services/api';
|
|
14
|
+
|
|
15
|
+
// ─── Finding the company by name, because that is what people know ───────────
|
|
16
|
+
//
|
|
17
|
+
// A registration number is on a certificate in a drawer; the name is in the
|
|
18
|
+
// applicant's head. Asking for the number first turns the very first field
|
|
19
|
+
// into a search of their own filing cabinet, which is where KYB applications
|
|
20
|
+
// are abandoned.
|
|
21
|
+
//
|
|
22
|
+
// The search itself is FREE and read-only; only picking a result leads to the
|
|
23
|
+
// paid register check. It fires on an explicit submit, never on keystroke: a
|
|
24
|
+
// debounce spends the register's small, uncontracted rate-limit budget on
|
|
25
|
+
// every prefix nobody asked for. Sizes and rhythm mirror the web SDK's
|
|
26
|
+
// BusinessSearch (12px block gaps, 48px input/button) — keep them in lockstep.
|
|
27
|
+
|
|
28
|
+
type SearchState =
|
|
29
|
+
| { phase: 'idle' }
|
|
30
|
+
| { phase: 'searching' }
|
|
31
|
+
| { phase: 'results'; hits: BusinessSearchHit[]; truncated: boolean }
|
|
32
|
+
| { phase: 'unavailable' };
|
|
33
|
+
|
|
34
|
+
export function BusinessSearch({
|
|
35
|
+
country,
|
|
36
|
+
onPicked,
|
|
37
|
+
onManualEntry,
|
|
38
|
+
}: {
|
|
39
|
+
country: string;
|
|
40
|
+
onPicked: (hit: BusinessSearchHit) => void;
|
|
41
|
+
onManualEntry: () => void;
|
|
42
|
+
}): React.ReactElement {
|
|
43
|
+
const store = useKycStore();
|
|
44
|
+
const { colors } = useTheme();
|
|
45
|
+
const region = useKyc((s) => s.business.subdivisionCode);
|
|
46
|
+
const [query, setQuery] = useState('');
|
|
47
|
+
// Narrows what came back; it never replaces the search. Matches the name AND
|
|
48
|
+
// the number — a box inviting either that answered only one looked broken.
|
|
49
|
+
const [filter, setFilter] = useState('');
|
|
50
|
+
const [state, setState] = useState<SearchState>({ phase: 'idle' });
|
|
51
|
+
// Four countries file companies per state or emirate, and the provider
|
|
52
|
+
// searches ONE register at a time. Without the region it fails closed —
|
|
53
|
+
// a company filed a state away would otherwise come back "not registered".
|
|
54
|
+
const [regions, setRegions] = useState<{ code: string; name: string }[]>([]);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
let cancelled = false;
|
|
58
|
+
setRegions([]);
|
|
59
|
+
if (!country) return undefined;
|
|
60
|
+
store
|
|
61
|
+
.getState()
|
|
62
|
+
.api.businessRegions(country)
|
|
63
|
+
.then((r) => {
|
|
64
|
+
if (!cancelled) setRegions(r.regions ?? []);
|
|
65
|
+
})
|
|
66
|
+
// An empty list means "do not ask" — also the safe answer when the
|
|
67
|
+
// catalogue is unreachable: the search itself says so on its own.
|
|
68
|
+
.catch(() => undefined);
|
|
69
|
+
return () => {
|
|
70
|
+
cancelled = true;
|
|
71
|
+
};
|
|
72
|
+
}, [country, store]);
|
|
73
|
+
|
|
74
|
+
const run = async (): Promise<void> => {
|
|
75
|
+
const q = query.trim();
|
|
76
|
+
if (q.length < 2) return;
|
|
77
|
+
if (regions.length > 0 && !region) return;
|
|
78
|
+
setState({ phase: 'searching' });
|
|
79
|
+
try {
|
|
80
|
+
const res = await store.getState().api.businessSearch({
|
|
81
|
+
country,
|
|
82
|
+
query: q,
|
|
83
|
+
limit: 50,
|
|
84
|
+
...(region ? { subdivisionCode: region } : {}),
|
|
85
|
+
});
|
|
86
|
+
const hits = res.results ?? [];
|
|
87
|
+
setFilter('');
|
|
88
|
+
setState({ phase: 'results', hits, truncated: hits.length >= 50 });
|
|
89
|
+
} catch {
|
|
90
|
+
// Deliberately NOT an empty result list. "No matches" reads as "this
|
|
91
|
+
// business is not registered", a claim an outage has not earned.
|
|
92
|
+
setState({ phase: 'unavailable' });
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const searching = state.phase === 'searching';
|
|
97
|
+
const shown =
|
|
98
|
+
state.phase === 'results'
|
|
99
|
+
? state.hits.filter((h) => {
|
|
100
|
+
const q = filter.trim().toLowerCase();
|
|
101
|
+
if (!q) return true;
|
|
102
|
+
return (
|
|
103
|
+
h.name.toLowerCase().includes(q) || h.registrationNumber.toLowerCase().includes(q)
|
|
104
|
+
);
|
|
105
|
+
})
|
|
106
|
+
: [];
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<View>
|
|
110
|
+
{regions.length > 0 && (
|
|
111
|
+
<View style={{ marginBottom: 12 }}>
|
|
112
|
+
<MyazaText variant="label" style={{ marginBottom: spacing.sm }}>
|
|
113
|
+
State or region of registration
|
|
114
|
+
</MyazaText>
|
|
115
|
+
<MyazaSelect
|
|
116
|
+
value={region || undefined}
|
|
117
|
+
sheetTitle="State or region"
|
|
118
|
+
searchable
|
|
119
|
+
options={regions.map((r) => ({ value: r.code, label: r.name }))}
|
|
120
|
+
onChange={(code) => store.getState().setBusinessField('subdivisionCode', code)}
|
|
121
|
+
/>
|
|
122
|
+
</View>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{/* No label: the placeholder says it, and the magnifier says what kind
|
|
126
|
+
of field this is — the web SDK's exact arrangement. */}
|
|
127
|
+
<MyazaInput
|
|
128
|
+
value={query}
|
|
129
|
+
onChangeText={setQuery}
|
|
130
|
+
placeholder="Search by company name"
|
|
131
|
+
autoCorrect={false}
|
|
132
|
+
prefix={<Icon name="search" size={16} color={colors.textMuted} />}
|
|
133
|
+
/>
|
|
134
|
+
<View style={{ height: 12 }} />
|
|
135
|
+
{/* The spinner lives IN the button, as on web — a separate spinner row
|
|
136
|
+
pushed the results down and said the same thing twice. */}
|
|
137
|
+
<MyazaButton
|
|
138
|
+
label={searching ? 'Searching…' : 'Search'}
|
|
139
|
+
leadingIcon={searching ? undefined : 'search'}
|
|
140
|
+
loading={searching}
|
|
141
|
+
onPress={() => void run()}
|
|
142
|
+
disabled={searching || query.trim().length < 2 || (regions.length > 0 && !region)}
|
|
143
|
+
/>
|
|
144
|
+
|
|
145
|
+
{state.phase === 'results' && state.hits.length > 0 && (
|
|
146
|
+
<BusinessSearchResults
|
|
147
|
+
country={country}
|
|
148
|
+
hits={state.hits}
|
|
149
|
+
shown={shown}
|
|
150
|
+
truncated={state.truncated}
|
|
151
|
+
filter={filter}
|
|
152
|
+
onFilter={setFilter}
|
|
153
|
+
onPicked={onPicked}
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{state.phase === 'results' && state.hits.length === 0 && (
|
|
158
|
+
<MyazaText variant="bodyMedium" color={colors.textSecondary} style={{ marginTop: 12 }}>
|
|
159
|
+
Nothing found under that name. Try a shorter version of it, or enter the details
|
|
160
|
+
yourself.
|
|
161
|
+
</MyazaText>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{state.phase === 'unavailable' && (
|
|
165
|
+
<View style={{ marginTop: 12 }}>
|
|
166
|
+
<MyazaAlert
|
|
167
|
+
variant="warning"
|
|
168
|
+
title="Search is unavailable"
|
|
169
|
+
message="We could not reach the company register just now. Try again, or enter the details yourself and we will check them when you submit."
|
|
170
|
+
/>
|
|
171
|
+
</View>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* Always offered, not only on failure: some companies are not in the
|
|
175
|
+
index at all, and a dead end at the first step ends the application. */}
|
|
176
|
+
<Pressable
|
|
177
|
+
accessibilityRole="button"
|
|
178
|
+
onPress={onManualEntry}
|
|
179
|
+
hitSlop={8}
|
|
180
|
+
style={{
|
|
181
|
+
flexDirection: 'row',
|
|
182
|
+
alignItems: 'center',
|
|
183
|
+
gap: 6,
|
|
184
|
+
alignSelf: 'flex-start',
|
|
185
|
+
marginTop: 12,
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
<Icon name="pencil" size={14} color={colors.textSecondary} />
|
|
189
|
+
<MyazaText
|
|
190
|
+
variant="bodyMedium"
|
|
191
|
+
color={colors.textSecondary}
|
|
192
|
+
style={{ fontWeight: '500' }}
|
|
193
|
+
>
|
|
194
|
+
Enter the details myself
|
|
195
|
+
</MyazaText>
|
|
196
|
+
</Pressable>
|
|
197
|
+
</View>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, ScrollView, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { CountryFlag } from '../components/CountryFlag';
|
|
5
|
+
import { Icon } from '../components/Icon';
|
|
6
|
+
import { MyazaInput } from '../components/MyazaInput';
|
|
7
|
+
import { MyazaText } from '../components/Typography';
|
|
8
|
+
import { useTheme } from '../components/runtime';
|
|
9
|
+
import { radius } from '../config/theme';
|
|
10
|
+
import type { BusinessSearchHit } from '../services/api';
|
|
11
|
+
|
|
12
|
+
// The result list: count first ("40 results" says whether to narrow before you
|
|
13
|
+
// start reading), a filter that narrows without re-searching, and one row per
|
|
14
|
+
// candidate. Split from BusinessSearch (200-line rule). Every dimension here —
|
|
15
|
+
// 40px filter, 12px row padding, 6px row gaps, 256px list — is the web SDK's,
|
|
16
|
+
// so the two render the same screen.
|
|
17
|
+
|
|
18
|
+
export function BusinessSearchResults({
|
|
19
|
+
country,
|
|
20
|
+
hits,
|
|
21
|
+
shown,
|
|
22
|
+
truncated,
|
|
23
|
+
filter,
|
|
24
|
+
onFilter,
|
|
25
|
+
onPicked,
|
|
26
|
+
}: {
|
|
27
|
+
country: string;
|
|
28
|
+
hits: BusinessSearchHit[];
|
|
29
|
+
shown: BusinessSearchHit[];
|
|
30
|
+
truncated: boolean;
|
|
31
|
+
filter: string;
|
|
32
|
+
onFilter: (value: string) => void;
|
|
33
|
+
onPicked: (hit: BusinessSearchHit) => void;
|
|
34
|
+
}): React.ReactElement {
|
|
35
|
+
const { colors } = useTheme();
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
{/* Count first: "40 results" says whether to narrow before you start
|
|
39
|
+
reading, which a bare list does not. */}
|
|
40
|
+
<View
|
|
41
|
+
style={{
|
|
42
|
+
flexDirection: 'row',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
justifyContent: 'space-between',
|
|
45
|
+
gap: 12,
|
|
46
|
+
marginTop: 12,
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<MyazaText variant="bodyMedium" color={colors.textSecondary}>
|
|
50
|
+
{shown.length === hits.length
|
|
51
|
+
? `${hits.length} results`
|
|
52
|
+
: `${shown.length} of ${hits.length} results`}
|
|
53
|
+
</MyazaText>
|
|
54
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
|
55
|
+
<MyazaText variant="bodyMedium" color={colors.textSecondary}>
|
|
56
|
+
Filter
|
|
57
|
+
</MyazaText>
|
|
58
|
+
<View style={{ width: 176 }}>
|
|
59
|
+
<MyazaInput
|
|
60
|
+
value={filter}
|
|
61
|
+
onChangeText={onFilter}
|
|
62
|
+
placeholder="Name or number"
|
|
63
|
+
autoCorrect={false}
|
|
64
|
+
height={40}
|
|
65
|
+
fontSize={14}
|
|
66
|
+
prefix={<Icon name="filter" size={14} color={colors.textMuted} />}
|
|
67
|
+
/>
|
|
68
|
+
</View>
|
|
69
|
+
</View>
|
|
70
|
+
</View>
|
|
71
|
+
<ScrollView style={{ maxHeight: 256, marginTop: 12 }} nestedScrollEnabled>
|
|
72
|
+
{shown.map((hit) => (
|
|
73
|
+
<Pressable
|
|
74
|
+
key={`${hit.registrationNumber}-${hit.name}`}
|
|
75
|
+
accessibilityRole="button"
|
|
76
|
+
onPress={() => onPicked(hit)}
|
|
77
|
+
style={{
|
|
78
|
+
flexDirection: 'row',
|
|
79
|
+
alignItems: 'center',
|
|
80
|
+
gap: 12,
|
|
81
|
+
borderWidth: 1,
|
|
82
|
+
borderColor: colors.border,
|
|
83
|
+
borderRadius: radius.sm,
|
|
84
|
+
padding: 12,
|
|
85
|
+
marginBottom: 6,
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<Icon name="building-2" size={16} color={colors.textSecondary} />
|
|
89
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
90
|
+
<MyazaText variant="label" color={colors.textDark} numberOfLines={1}>
|
|
91
|
+
{hit.name}
|
|
92
|
+
</MyazaText>
|
|
93
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, marginTop: 2 }}>
|
|
94
|
+
<CountryFlag country={country} size={14} />
|
|
95
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary}>
|
|
96
|
+
{hit.registrationNumber}
|
|
97
|
+
</MyazaText>
|
|
98
|
+
</View>
|
|
99
|
+
</View>
|
|
100
|
+
</Pressable>
|
|
101
|
+
))}
|
|
102
|
+
</ScrollView>
|
|
103
|
+
{shown.length === 0 && (
|
|
104
|
+
<MyazaText variant="bodyMedium" color={colors.textSecondary} style={{ marginTop: 12 }}>
|
|
105
|
+
Nothing here matches "{filter.trim()}". Clear the filter to see all {hits.length}.
|
|
106
|
+
</MyazaText>
|
|
107
|
+
)}
|
|
108
|
+
{truncated && (
|
|
109
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: 6 }}>
|
|
110
|
+
Showing the first {hits.length}. Add more of the name to narrow it down.
|
|
111
|
+
</MyazaText>
|
|
112
|
+
)}
|
|
113
|
+
</>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -5,79 +5,119 @@ import { spacing } from '../config/theme';
|
|
|
5
5
|
import { useTheme } from '../components/runtime';
|
|
6
6
|
import { MyazaText } from '../components/Typography';
|
|
7
7
|
import { MyazaInput } from '../components/MyazaInput';
|
|
8
|
+
import { MyazaDateField } from '../components/MyazaDateField';
|
|
9
|
+
import { PhoneNumberInput } from '../components/PhoneNumberInput';
|
|
10
|
+
import { isValidContactEmail } from '../config/contact';
|
|
11
|
+
import { isValidWebsite } from '../config/website';
|
|
8
12
|
import type { CompanyInfoField, CompanyInfoMode } from '../types/business';
|
|
13
|
+
import type { BusinessState } from '../store/state';
|
|
9
14
|
|
|
10
15
|
// ---------------------------------------------------------------------------
|
|
11
16
|
// The company profile a KYB workflow asks for.
|
|
12
17
|
//
|
|
13
18
|
// Split from BusinessDetailsStep (200-line rule). Each field's mode comes from
|
|
14
19
|
// the workflow and must match the server's resolution exactly — a field marked
|
|
15
|
-
// optional here but required there produces a 422 the user cannot act on.
|
|
16
|
-
//
|
|
17
|
-
//
|
|
20
|
+
// optional here but required there produces a 422 the user cannot act on. The
|
|
21
|
+
// last five are registry facts the applicant STATES: asked as their own answer
|
|
22
|
+
// rather than filled from the register, because where the two differ that is
|
|
23
|
+
// the finding. Field list, labels, placeholders and controls mirror the web
|
|
24
|
+
// SDK's BusinessCompanyInfoFields 1:1.
|
|
18
25
|
// ---------------------------------------------------------------------------
|
|
19
26
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
27
|
+
const FIELD_DEFS: Array<{
|
|
28
|
+
key: CompanyInfoField;
|
|
29
|
+
label: string;
|
|
30
|
+
placeholder: string;
|
|
31
|
+
kind?: 'phone' | 'date' | 'email' | 'website';
|
|
32
|
+
}> = [
|
|
33
|
+
{ key: 'address', label: 'Registered address', placeholder: 'e.g. 12 Marina Road, Lagos' },
|
|
34
|
+
{ key: 'email', label: 'Business email', placeholder: 'hello@company.com', kind: 'email' },
|
|
35
|
+
{ key: 'phone', label: 'Business phone', placeholder: '+234 800 000 0000', kind: 'phone' },
|
|
36
|
+
{ key: 'website', label: 'Website', placeholder: 'company.com', kind: 'website' },
|
|
37
|
+
{ key: 'dateOfIncorporation', label: 'Date of incorporation', placeholder: 'YYYY-MM-DD', kind: 'date' },
|
|
38
|
+
{ key: 'taxId', label: 'Tax ID', placeholder: 'e.g. 01234567-0001' },
|
|
39
|
+
{ key: 'vatNumber', label: 'VAT number', placeholder: 'e.g. NG123456789' },
|
|
40
|
+
{ key: 'companyType', label: 'Company type', placeholder: 'e.g. Private Limited Company' },
|
|
41
|
+
{ key: 'natureOfBusiness', label: 'Nature of business', placeholder: 'What the company does' },
|
|
42
|
+
];
|
|
33
43
|
|
|
34
44
|
export function CompanyInfoFields({
|
|
35
|
-
fields,
|
|
36
|
-
modes,
|
|
37
45
|
values,
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
modes,
|
|
47
|
+
country,
|
|
48
|
+
geoCountry,
|
|
40
49
|
onChange,
|
|
41
50
|
}: {
|
|
42
|
-
|
|
51
|
+
values: BusinessState;
|
|
43
52
|
modes: Record<CompanyInfoField, CompanyInfoMode>;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
/** Seeds the phone dial code: the company's country of registration. */
|
|
54
|
+
country?: string;
|
|
55
|
+
/** The visitor's IP country: the dial-code seed of last resort, and the
|
|
56
|
+
* row pinned to the top of the picker. */
|
|
57
|
+
geoCountry?: string | null;
|
|
47
58
|
onChange: (field: CompanyInfoField, value: string) => void;
|
|
48
59
|
}): React.ReactElement | null {
|
|
49
60
|
const { colors } = useTheme();
|
|
50
|
-
|
|
61
|
+
const visible = FIELD_DEFS.filter((f) => modes[f.key] !== 'off');
|
|
62
|
+
if (visible.length === 0) return null;
|
|
51
63
|
|
|
52
64
|
return (
|
|
53
|
-
|
|
54
|
-
<
|
|
55
|
-
<MyazaText variant="bodyMedium" style={{ fontWeight: '600' }}>
|
|
56
|
-
Company information
|
|
57
|
-
</MyazaText>
|
|
65
|
+
<View>
|
|
66
|
+
<MyazaText variant="label">Company information</MyazaText>
|
|
58
67
|
<MyazaText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: 2 }}>
|
|
59
68
|
We verify these details against the official registry record.
|
|
60
69
|
</MyazaText>
|
|
61
|
-
{
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
?
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
70
|
+
{visible.map((f) => {
|
|
71
|
+
const required = modes[f.key] === 'required';
|
|
72
|
+
const value = values[f.key];
|
|
73
|
+
const error =
|
|
74
|
+
f.kind === 'email' && value !== '' && !isValidContactEmail(value)
|
|
75
|
+
? 'Enter a valid email address.'
|
|
76
|
+
: f.kind === 'website' && value !== '' && !isValidWebsite(value)
|
|
77
|
+
? 'Enter a valid website, for example company.com'
|
|
78
|
+
: null;
|
|
79
|
+
return (
|
|
80
|
+
<View key={f.key} style={{ marginTop: spacing.md }}>
|
|
81
|
+
{f.kind === 'phone' || f.kind === 'date' ? (
|
|
82
|
+
<MyazaText variant="label" style={{ marginBottom: spacing.sm }}>
|
|
83
|
+
{f.label}
|
|
84
|
+
{required ? ' *' : ' (optional)'}
|
|
85
|
+
</MyazaText>
|
|
86
|
+
) : null}
|
|
87
|
+
{f.kind === 'phone' ? (
|
|
88
|
+
// The same control the phone-verification step uses: dial-code
|
|
89
|
+
// picker, as-you-type national formatting, E.164 out. A business
|
|
90
|
+
// number is a phone number, and a bare text box gets back a dozen
|
|
91
|
+
// different shapes of the same digits. `value` carries the
|
|
92
|
+
// register's number when the lookup returned one.
|
|
93
|
+
<PhoneNumberInput
|
|
94
|
+
value={value}
|
|
95
|
+
defaultCountry={country || geoCountry || undefined}
|
|
96
|
+
geoCountry={geoCountry}
|
|
97
|
+
onChange={({ e164 }) => onChange('phone', e164)}
|
|
98
|
+
/>
|
|
99
|
+
) : f.kind === 'date' ? (
|
|
100
|
+
// A date gets the picker, not a text box — same control the
|
|
101
|
+
// questionnaire uses; emits strict YYYY-MM-DD.
|
|
102
|
+
<MyazaDateField
|
|
103
|
+
value={value || undefined}
|
|
104
|
+
placeholder={f.placeholder}
|
|
105
|
+
onChange={(iso) => onChange(f.key, iso)}
|
|
106
|
+
/>
|
|
107
|
+
) : (
|
|
108
|
+
<MyazaInput
|
|
109
|
+
label={`${f.label}${required ? ' *' : ' (optional)'}`}
|
|
110
|
+
value={value}
|
|
111
|
+
onChangeText={(text) => onChange(f.key, text)}
|
|
112
|
+
placeholder={f.placeholder}
|
|
113
|
+
keyboardType={f.kind === 'email' ? 'email-address' : 'default'}
|
|
114
|
+
autoCapitalize={f.kind === 'email' || f.kind === 'website' ? 'none' : 'words'}
|
|
115
|
+
error={error}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
118
|
+
</View>
|
|
119
|
+
);
|
|
120
|
+
})}
|
|
121
|
+
</View>
|
|
82
122
|
);
|
|
83
123
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Linking, Pressable, View } from 'react-native';
|
|
3
|
-
import Svg, { Circle
|
|
3
|
+
import Svg, { Circle } from 'react-native-svg';
|
|
4
4
|
|
|
5
5
|
import { radius, spacing } from '../config/theme';
|
|
6
6
|
import { PRIVACY_URL, TERMS_URL } from '../config/brand';
|
|
@@ -9,6 +9,7 @@ import { MyazaText } from '../components/Typography';
|
|
|
9
9
|
import { MyazaButton } from '../components/MyazaButton';
|
|
10
10
|
import { Icon } from '../components/Icon';
|
|
11
11
|
import { buildConsentModel } from './consent/model';
|
|
12
|
+
import { resubmitNote } from '../lib/resubmit';
|
|
12
13
|
|
|
13
14
|
// Consent / welcome screen — 1:1 with the web SDK's ConsentStep and the
|
|
14
15
|
// Flutter SDK's ConsentScreen: shield hero, token-filled greeting, a "DURING
|
|
@@ -27,10 +28,40 @@ export function ConsentStep(): React.ReactElement {
|
|
|
27
28
|
const { isBusiness, title, description, steps, capturesFace, recordsVideo } =
|
|
28
29
|
buildConsentModel(config);
|
|
29
30
|
|
|
31
|
+
// A reviewer sent this applicant back. Say so, and say why — the note is the
|
|
32
|
+
// only thing on screen that explains a flow which has silently lost most of
|
|
33
|
+
// its steps. Above the hero so it is read before the instructions.
|
|
34
|
+
const redoNote = resubmitNote(config.resubmit);
|
|
35
|
+
|
|
30
36
|
return (
|
|
31
37
|
<View>
|
|
32
38
|
<View style={{ height: spacing.sm }} />
|
|
33
39
|
|
|
40
|
+
{redoNote ? (
|
|
41
|
+
<View
|
|
42
|
+
style={{
|
|
43
|
+
flexDirection: 'row',
|
|
44
|
+
gap: spacing.sm,
|
|
45
|
+
borderRadius: radius.lg,
|
|
46
|
+
borderWidth: 1,
|
|
47
|
+
borderColor: `${colors.warning}66`,
|
|
48
|
+
backgroundColor: colors.warningBg,
|
|
49
|
+
padding: spacing.md,
|
|
50
|
+
marginBottom: spacing.lg,
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<Icon name="refresh" size={16} color={colors.warning} />
|
|
54
|
+
<View style={{ flex: 1, gap: 2 }}>
|
|
55
|
+
<MyazaText variant="bodyMedium" color={colors.warning}>
|
|
56
|
+
A few things to redo
|
|
57
|
+
</MyazaText>
|
|
58
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary}>
|
|
59
|
+
{redoNote}
|
|
60
|
+
</MyazaText>
|
|
61
|
+
</View>
|
|
62
|
+
</View>
|
|
63
|
+
) : null}
|
|
64
|
+
|
|
34
65
|
{/* Shield hero — concentric tinted rings + primary badge */}
|
|
35
66
|
<View style={{ alignItems: 'center' }}>
|
|
36
67
|
<View style={{ width: 80, height: 80, alignItems: 'center', justifyContent: 'center' }}>
|
|
@@ -50,21 +81,10 @@ export function ConsentStep(): React.ReactElement {
|
|
|
50
81
|
elevation: 6,
|
|
51
82
|
}}
|
|
52
83
|
>
|
|
53
|
-
{/*
|
|
54
|
-
|
|
55
|
-
stop is the SAME primary at 70% opacity rather than a
|
|
56
|
-
pre-blended colour: composited over the sheet it produces
|
|
57
|
-
Flutter's `alphaBlend(primary@0.7, background)` exactly, and it
|
|
58
|
-
keeps working when the consumer overrides primaryColor or the
|
|
59
|
-
theme flips, which a hardcoded blend would not. */}
|
|
84
|
+
{/* Solid primary badge — no gradients in the flow (house rule
|
|
85
|
+
2026-08-29, applied across all three SDKs in lockstep). */}
|
|
60
86
|
<Svg width={56} height={56} style={{ position: 'absolute' }}>
|
|
61
|
-
<
|
|
62
|
-
<LinearGradient id="myazaShieldHero" x1="0" y1="0" x2="1" y2="1">
|
|
63
|
-
<Stop offset="0" stopColor={colors.primary} stopOpacity={1} />
|
|
64
|
-
<Stop offset="1" stopColor={colors.primary} stopOpacity={0.7} />
|
|
65
|
-
</LinearGradient>
|
|
66
|
-
</Defs>
|
|
67
|
-
<Circle cx={28} cy={28} r={28} fill="url(#myazaShieldHero)" />
|
|
87
|
+
<Circle cx={28} cy={28} r={28} fill={colors.primary} />
|
|
68
88
|
</Svg>
|
|
69
89
|
<Icon name="shield" size={28} color={colors.onPrimary} />
|
|
70
90
|
</View>
|
|
@@ -122,7 +142,7 @@ export function ConsentStep(): React.ReactElement {
|
|
|
122
142
|
<MyazaText
|
|
123
143
|
variant="bodySmall"
|
|
124
144
|
color={colors.textDark}
|
|
125
|
-
style={{ fontWeight: '
|
|
145
|
+
style={{ fontWeight: '500', textDecorationLine: 'underline' }}
|
|
126
146
|
onPress={() => void Linking.openURL(TERMS_URL).catch(() => undefined)}
|
|
127
147
|
>
|
|
128
148
|
End User Terms
|
|
@@ -131,7 +151,7 @@ export function ConsentStep(): React.ReactElement {
|
|
|
131
151
|
<MyazaText
|
|
132
152
|
variant="bodySmall"
|
|
133
153
|
color={colors.textDark}
|
|
134
|
-
style={{ fontWeight: '
|
|
154
|
+
style={{ fontWeight: '500', textDecorationLine: 'underline' }}
|
|
135
155
|
onPress={() => void Linking.openURL(PRIVACY_URL).catch(() => undefined)}
|
|
136
156
|
>
|
|
137
157
|
Privacy Policy
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../config/theme';
|
|
5
|
+
import { MyazaButton } from '../components/MyazaButton';
|
|
6
|
+
import { ContactFooterNote } from './ContactFooterNote';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// The contact step's footer: the one primary action (send, then verify), the
|
|
10
|
+
// optional skip, and the reassurance note. Split from ContactVerificationStep
|
|
11
|
+
// (200-line rule); the step owns every decision and this only renders them.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export function ContactActions({
|
|
15
|
+
hasChallenge,
|
|
16
|
+
busy,
|
|
17
|
+
disabled,
|
|
18
|
+
required,
|
|
19
|
+
isEmail,
|
|
20
|
+
onPrimary,
|
|
21
|
+
onSkip,
|
|
22
|
+
}: {
|
|
23
|
+
/** A code is out: the button verifies instead of sending. */
|
|
24
|
+
hasChallenge: boolean;
|
|
25
|
+
busy: boolean;
|
|
26
|
+
disabled: boolean;
|
|
27
|
+
/** `required: false` adds the skip; the server enforces the rest. */
|
|
28
|
+
required: boolean;
|
|
29
|
+
isEmail: boolean;
|
|
30
|
+
onPrimary: () => void;
|
|
31
|
+
onSkip: () => void;
|
|
32
|
+
}): React.ReactElement {
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<View style={{ height: spacing.md }} />
|
|
36
|
+
<MyazaButton
|
|
37
|
+
label={hasChallenge ? 'Verify code' : 'Send code'}
|
|
38
|
+
loading={busy}
|
|
39
|
+
disabled={disabled}
|
|
40
|
+
onPress={onPrimary}
|
|
41
|
+
/>
|
|
42
|
+
|
|
43
|
+
{!required ? (
|
|
44
|
+
<>
|
|
45
|
+
<View style={{ height: spacing.sm }} />
|
|
46
|
+
<MyazaButton label="Skip for now" variant="ghost" disabled={busy} onPress={onSkip} />
|
|
47
|
+
</>
|
|
48
|
+
) : null}
|
|
49
|
+
|
|
50
|
+
<View style={{ height: spacing.md }} />
|
|
51
|
+
<ContactFooterNote isEmail={isEmail} />
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -18,6 +18,7 @@ export function ContactDestinationField({
|
|
|
18
18
|
onEmailChange,
|
|
19
19
|
onPhoneChange,
|
|
20
20
|
defaultCountry,
|
|
21
|
+
geoCountry,
|
|
21
22
|
error,
|
|
22
23
|
disabled,
|
|
23
24
|
}: {
|
|
@@ -26,6 +27,8 @@ export function ContactDestinationField({
|
|
|
26
27
|
onEmailChange: (value: string) => void;
|
|
27
28
|
onPhoneChange: (value: { e164: string; isValid: boolean }) => void;
|
|
28
29
|
defaultCountry?: string;
|
|
30
|
+
/** The visitor's IP country, pinned to the top of the dial-code picker. */
|
|
31
|
+
geoCountry?: string | null;
|
|
29
32
|
error: string | null;
|
|
30
33
|
disabled?: boolean;
|
|
31
34
|
}): React.ReactElement {
|
|
@@ -52,7 +55,9 @@ export function ContactDestinationField({
|
|
|
52
55
|
</MyazaText>
|
|
53
56
|
<PhoneNumberInput
|
|
54
57
|
defaultCountry={defaultCountry}
|
|
58
|
+
geoCountry={geoCountry}
|
|
55
59
|
disabled={disabled}
|
|
60
|
+
autoFocus
|
|
56
61
|
onChange={onPhoneChange}
|
|
57
62
|
/>
|
|
58
63
|
{error ? <ErrorLine text={error} /> : null}
|