@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
|
@@ -13,8 +13,9 @@ import { useTheme } from './runtime';
|
|
|
13
13
|
import { MyazaText } from './Typography';
|
|
14
14
|
import { MyazaInput } from './MyazaInput';
|
|
15
15
|
import { Icon } from './Icon';
|
|
16
|
-
import { CountryFlag } from './CountryFlag';
|
|
17
16
|
import { FloatingSheet } from './glass/FloatingSheet';
|
|
17
|
+
import { DialCodeDivider, DialCodeRegionHeader, DialCodeRow } from './DialCodeRow';
|
|
18
|
+
import { buildDialCodeItems, dialCodeItemKey, filterDialCodeOptions, type DialCodeItem } from './dialCodeRows';
|
|
18
19
|
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// THE country sheet — the phone field's dial-code picker, generalised.
|
|
@@ -33,6 +34,10 @@ import { FloatingSheet } from './glass/FloatingSheet';
|
|
|
33
34
|
// The sheet is sized against the space left ABOVE the keyboard, like Flutter's.
|
|
34
35
|
// The search field autofocuses, so measuring against the full screen would put
|
|
35
36
|
// every result underneath the keys the moment the user started typing.
|
|
37
|
+
//
|
|
38
|
+
// `grouped` lists the countries under region headers (Africa first), the way
|
|
39
|
+
// the country-select step does — what the address-scope country control asks
|
|
40
|
+
// for. The list itself is built by dialCodeRows.ts.
|
|
36
41
|
// ---------------------------------------------------------------------------
|
|
37
42
|
|
|
38
43
|
export interface DialCodeOption {
|
|
@@ -46,6 +51,8 @@ export function DialCodePicker({
|
|
|
46
51
|
visible,
|
|
47
52
|
options,
|
|
48
53
|
selected,
|
|
54
|
+
pinned,
|
|
55
|
+
grouped = false,
|
|
49
56
|
onPick,
|
|
50
57
|
onClose,
|
|
51
58
|
searchPlaceholder = 'Search country or code',
|
|
@@ -53,6 +60,14 @@ export function DialCodePicker({
|
|
|
53
60
|
visible: boolean;
|
|
54
61
|
options: DialCodeOption[];
|
|
55
62
|
selected: string;
|
|
63
|
+
/**
|
|
64
|
+
* The visitor's IP country. Lifted out of the alphabet to the top and
|
|
65
|
+
* tagged, so a guess we made on their behalf is visible AS a guess and one
|
|
66
|
+
* tap away rather than buried among two hundred others.
|
|
67
|
+
*/
|
|
68
|
+
pinned?: string | null;
|
|
69
|
+
/** Region headers between the rows (the pinned row stays on top). */
|
|
70
|
+
grouped?: boolean;
|
|
56
71
|
onPick: (code: string) => void;
|
|
57
72
|
onClose: () => void;
|
|
58
73
|
searchPlaceholder?: string;
|
|
@@ -78,19 +93,13 @@ export function DialCodePicker({
|
|
|
78
93
|
const available = screenHeight - keyboard;
|
|
79
94
|
const maxHeight = Math.min(Math.max(available * 0.85, 240), screenHeight * 0.6);
|
|
80
95
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
(o) =>
|
|
89
|
-
o.name.toLowerCase().includes(q) ||
|
|
90
|
-
o.code.toLowerCase() === q ||
|
|
91
|
-
(o.dialCode != null && o.dialCode.replace(/^\+/, '').startsWith(bare)),
|
|
92
|
-
);
|
|
93
|
-
}, [options, query]);
|
|
96
|
+
// Where they appear to be, lifted out of the alphabet. It stays subject to
|
|
97
|
+
// the search, so typing still narrows to what was asked for rather than
|
|
98
|
+
// keeping a row that does not match.
|
|
99
|
+
const rows = useMemo(
|
|
100
|
+
() => buildDialCodeItems(filterDialCodeOptions(options, query), pinned, grouped),
|
|
101
|
+
[options, query, pinned, grouped],
|
|
102
|
+
);
|
|
94
103
|
|
|
95
104
|
const close = (): void => {
|
|
96
105
|
setQuery('');
|
|
@@ -106,75 +115,52 @@ export function DialCodePicker({
|
|
|
106
115
|
bottomOffset={keyboard}
|
|
107
116
|
closeLabel="Close country picker"
|
|
108
117
|
>
|
|
109
|
-
|
|
118
|
+
{/* Clear air between the header row (handle + close) and the search
|
|
119
|
+
field — flush against the close button they read as one control. */}
|
|
120
|
+
<View style={{ paddingHorizontal: spacing.md, paddingTop: spacing.md, paddingBottom: spacing.sm }}>
|
|
121
|
+
{/* Deliberately NOT autofocused: springing the keyboard the instant
|
|
122
|
+
the sheet opens hides half the list before the person has even
|
|
123
|
+
seen it — search is one tap away for whoever wants it. */}
|
|
110
124
|
<MyazaInput
|
|
111
125
|
value={query}
|
|
112
126
|
onChangeText={setQuery}
|
|
113
127
|
placeholder={searchPlaceholder}
|
|
114
|
-
autoFocus
|
|
115
128
|
prefix={<Icon name="search" size={18} color={colors.textSecondary} />}
|
|
116
129
|
/>
|
|
117
130
|
</View>
|
|
118
131
|
|
|
119
132
|
<FlatList
|
|
120
|
-
data={
|
|
121
|
-
keyExtractor={
|
|
133
|
+
data={rows.items}
|
|
134
|
+
keyExtractor={dialCodeItemKey}
|
|
122
135
|
keyboardShouldPersistTaps="handled"
|
|
136
|
+
// A hairline under the pinned row only: the alphabet below it is one
|
|
137
|
+
// list, and a rule between every row is noise.
|
|
138
|
+
ItemSeparatorComponent={({ leadingItem }: { leadingItem: DialCodeItem }) =>
|
|
139
|
+
rows.geo != null && leadingItem.kind === 'row' && leadingItem.option.code === rows.geo.code ? (
|
|
140
|
+
<DialCodeDivider />
|
|
141
|
+
) : null
|
|
142
|
+
}
|
|
123
143
|
ListEmptyComponent={
|
|
124
144
|
<MyazaText variant="bodyMedium" style={{ padding: spacing.lg, textAlign: 'center' }}>
|
|
125
145
|
No countries match your search.
|
|
126
146
|
</MyazaText>
|
|
127
147
|
}
|
|
128
|
-
renderItem={({ item }) =>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
148
|
+
renderItem={({ item }) =>
|
|
149
|
+
item.kind === 'header' ? (
|
|
150
|
+
<DialCodeRegionHeader region={item.region} />
|
|
151
|
+
) : (
|
|
152
|
+
<DialCodeRow
|
|
153
|
+
option={item.option}
|
|
154
|
+
isSelected={item.option.code === selected}
|
|
155
|
+
badge={rows.geo != null && item.option.code === rows.geo.code ? 'Your location' : undefined}
|
|
156
|
+
onPress={() => {
|
|
157
|
+
setQuery('');
|
|
158
|
+
onPick(item.option.code);
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
138
163
|
/>
|
|
139
164
|
</FloatingSheet>
|
|
140
165
|
);
|
|
141
166
|
}
|
|
142
|
-
|
|
143
|
-
function CountryRow({
|
|
144
|
-
option,
|
|
145
|
-
isSelected,
|
|
146
|
-
onPress,
|
|
147
|
-
}: {
|
|
148
|
-
option: DialCodeOption;
|
|
149
|
-
isSelected: boolean;
|
|
150
|
-
onPress: () => void;
|
|
151
|
-
}): React.ReactElement {
|
|
152
|
-
const { colors } = useTheme();
|
|
153
|
-
return (
|
|
154
|
-
<Pressable
|
|
155
|
-
onPress={onPress}
|
|
156
|
-
accessibilityRole="radio"
|
|
157
|
-
accessibilityState={{ checked: isSelected }}
|
|
158
|
-
style={{
|
|
159
|
-
flexDirection: 'row',
|
|
160
|
-
alignItems: 'center',
|
|
161
|
-
paddingHorizontal: spacing.md,
|
|
162
|
-
paddingVertical: spacing.sm,
|
|
163
|
-
backgroundColor: isSelected ? colors.primary50 : 'transparent',
|
|
164
|
-
}}
|
|
165
|
-
>
|
|
166
|
-
<CountryFlag country={option.code} size={24} />
|
|
167
|
-
<View style={{ width: spacing.md }} />
|
|
168
|
-
{/* No line clamp: "Bosnia & Herzegovina" and the like must read in full,
|
|
169
|
-
which is what Flutter's Expanded(Text) gives. */}
|
|
170
|
-
<MyazaText variant="bodyMedium" style={{ flex: 1 }}>
|
|
171
|
-
{option.name}
|
|
172
|
-
</MyazaText>
|
|
173
|
-
{option.dialCode != null ? (
|
|
174
|
-
<MyazaText variant="label" color={colors.textSecondary}>
|
|
175
|
-
{option.dialCode}
|
|
176
|
-
</MyazaText>
|
|
177
|
-
) : null}
|
|
178
|
-
</Pressable>
|
|
179
|
-
);
|
|
180
|
-
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, StyleSheet, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../config/theme';
|
|
5
|
+
import { useTheme } from './runtime';
|
|
6
|
+
import { MyazaText } from './Typography';
|
|
7
|
+
import { CountryFlag } from './CountryFlag';
|
|
8
|
+
import { GeoBadge } from './GeoBadge';
|
|
9
|
+
import type { DialCodeOption } from './DialCodePicker';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// One country in the dial-code sheet. Extracted from DialCodePicker (200-line
|
|
13
|
+
// rule) and shared by the pinned geo row and the alphabetical ones, so the two
|
|
14
|
+
// cannot drift apart: they are the same control in two positions, and the
|
|
15
|
+
// pinned one only differs by carrying a tag and a hairline beneath it.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export function DialCodeRow({
|
|
19
|
+
option,
|
|
20
|
+
isSelected,
|
|
21
|
+
badge,
|
|
22
|
+
onPress,
|
|
23
|
+
}: {
|
|
24
|
+
option: DialCodeOption;
|
|
25
|
+
isSelected: boolean;
|
|
26
|
+
/** Tags the pinned geo row ("Your location"). */
|
|
27
|
+
badge?: string;
|
|
28
|
+
onPress: () => void;
|
|
29
|
+
}): React.ReactElement {
|
|
30
|
+
const { colors } = useTheme();
|
|
31
|
+
return (
|
|
32
|
+
<Pressable
|
|
33
|
+
onPress={onPress}
|
|
34
|
+
accessibilityRole="radio"
|
|
35
|
+
accessibilityState={{ checked: isSelected }}
|
|
36
|
+
accessibilityLabel={badge ? `${option.name}, ${badge.toLowerCase()}` : option.name}
|
|
37
|
+
style={{
|
|
38
|
+
flexDirection: 'row',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
paddingHorizontal: spacing.md,
|
|
41
|
+
// Roomier rows: a country list is a long scan, and cramped rows make
|
|
42
|
+
// every tap a precision job. Matches the web dropdown's row height.
|
|
43
|
+
paddingVertical: 12,
|
|
44
|
+
backgroundColor: isSelected ? colors.primary50 : 'transparent',
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<CountryFlag country={option.code} size={28} />
|
|
48
|
+
<View style={{ width: spacing.md }} />
|
|
49
|
+
{/* No line clamp: "Bosnia & Herzegovina" and the like must read in full,
|
|
50
|
+
which is what Flutter's Expanded(Text) gives. Full body size (16),
|
|
51
|
+
the web dropdown's reading size — 14 read small against the flags. */}
|
|
52
|
+
<MyazaText variant="body" style={{ flex: 1 }}>
|
|
53
|
+
{option.name}
|
|
54
|
+
</MyazaText>
|
|
55
|
+
{badge ? <GeoBadge label={badge} /> : null}
|
|
56
|
+
{option.dialCode != null ? (
|
|
57
|
+
<MyazaText variant="bodyMedium" color={colors.textSecondary}>
|
|
58
|
+
{option.dialCode}
|
|
59
|
+
</MyazaText>
|
|
60
|
+
) : null}
|
|
61
|
+
</Pressable>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A region label between grouped rows — the country-select step's header
|
|
67
|
+
* (uppercase, tracked out), so the two pickers read as one control.
|
|
68
|
+
*/
|
|
69
|
+
export function DialCodeRegionHeader({ region }: { region: string }): React.ReactElement {
|
|
70
|
+
const { colors } = useTheme();
|
|
71
|
+
return (
|
|
72
|
+
<MyazaText
|
|
73
|
+
variant="bodySmall"
|
|
74
|
+
color={colors.textSecondary}
|
|
75
|
+
accessibilityRole="header"
|
|
76
|
+
style={{
|
|
77
|
+
fontWeight: '700',
|
|
78
|
+
letterSpacing: 0.6,
|
|
79
|
+
paddingHorizontal: spacing.md,
|
|
80
|
+
paddingTop: spacing.md,
|
|
81
|
+
paddingBottom: spacing.xs,
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{region.toUpperCase()}
|
|
85
|
+
</MyazaText>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The hairline under the pinned row, separating it from the alphabet. */
|
|
90
|
+
export function DialCodeDivider(): React.ReactElement {
|
|
91
|
+
const { colors } = useTheme();
|
|
92
|
+
return (
|
|
93
|
+
<View
|
|
94
|
+
style={{
|
|
95
|
+
marginHorizontal: spacing.md,
|
|
96
|
+
marginVertical: spacing.xs,
|
|
97
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
98
|
+
borderBottomColor: colors.border,
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { ActivityIndicator, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius } from '../config/theme';
|
|
5
|
+
import { loadWebView, type WebViewLike } from '../lib/webview-available';
|
|
6
|
+
import { useTheme } from './runtime';
|
|
7
|
+
import { MapPinPicker } from './MapPinPicker';
|
|
8
|
+
import {
|
|
9
|
+
MAP_FRAME_READY_TIMEOUT_MS,
|
|
10
|
+
buildMapFrameSrc,
|
|
11
|
+
centerCommandScript,
|
|
12
|
+
parseMapFrameMessage,
|
|
13
|
+
samePin,
|
|
14
|
+
} from '../lib/map-frame';
|
|
15
|
+
import type { LatLng } from '../lib/map-tiles';
|
|
16
|
+
|
|
17
|
+
// Google Maps on mobile, via OUR hosted /embed/map page in a WebView (the OkHi
|
|
18
|
+
// model — the map runs on the hosted origin, on Myaza's own key; the host app
|
|
19
|
+
// never brings one). Mirrors the web SDK's FramedMapPicker: a page that never
|
|
20
|
+
// says `ready` (no webview module installed, blocked script, refused grant,
|
|
21
|
+
// vendor outage) falls back to the dependency-free OSM picker, so the step can
|
|
22
|
+
// never go blank. `react-native-webview` is an OPTIONAL peer: absent, this
|
|
23
|
+
// component IS the OSM picker.
|
|
24
|
+
|
|
25
|
+
interface FramedMapPickerProps {
|
|
26
|
+
/** The server-minted page URL; null ⇒ the OSM picker outright. */
|
|
27
|
+
frameUrl: string | null;
|
|
28
|
+
value: LatLng | null;
|
|
29
|
+
/** Corner rounding. 0 where a parent already shapes the frame — the review
|
|
30
|
+
* card rounds its TOP corners only, and the picker's own radius put a curve
|
|
31
|
+
* where the map meets the address band. */
|
|
32
|
+
cornerRadius?: number;
|
|
33
|
+
onChange: (pin: LatLng) => void;
|
|
34
|
+
defaultCenter: LatLng;
|
|
35
|
+
defaultZoom: number;
|
|
36
|
+
height?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function FramedMapPicker({ frameUrl, value, onChange, defaultCenter, defaultZoom, height = 300, cornerRadius }: FramedMapPickerProps): React.ReactElement {
|
|
40
|
+
const WebView = useMemo(loadWebView, []);
|
|
41
|
+
const { colors, mode } = useTheme();
|
|
42
|
+
const [ready, setReady] = useState(false);
|
|
43
|
+
const [failed, setFailed] = useState(!frameUrl || !WebView);
|
|
44
|
+
const readyRef = useRef(false);
|
|
45
|
+
const webRef = useRef<WebViewLike>(null);
|
|
46
|
+
const onChangeRef = useRef(onChange);
|
|
47
|
+
onChangeRef.current = onChange;
|
|
48
|
+
const lastFromFrame = useRef<LatLng | null>(null);
|
|
49
|
+
|
|
50
|
+
// Built once per mount: a src that tracked the pin would reload the page per drag.
|
|
51
|
+
const src = useMemo(
|
|
52
|
+
() =>
|
|
53
|
+
frameUrl
|
|
54
|
+
? buildMapFrameSrc(frameUrl, {
|
|
55
|
+
center: value ?? defaultCenter,
|
|
56
|
+
zoom: defaultZoom,
|
|
57
|
+
hasPin: value != null,
|
|
58
|
+
theme: mode === 'dark' ? 'dark' : 'light',
|
|
59
|
+
primaryColor: colors.primary,
|
|
60
|
+
})
|
|
61
|
+
: null,
|
|
62
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
63
|
+
[frameUrl],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (failed) return undefined;
|
|
68
|
+
const timeout = setTimeout(() => {
|
|
69
|
+
if (!readyRef.current) setFailed(true);
|
|
70
|
+
}, MAP_FRAME_READY_TIMEOUT_MS);
|
|
71
|
+
return () => clearTimeout(timeout);
|
|
72
|
+
}, [failed]);
|
|
73
|
+
|
|
74
|
+
// An external recentre (Use my location, restored progress) moves the map —
|
|
75
|
+
// but never echo back a pin the page itself just reported.
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!value || !ready || samePin(lastFromFrame.current, value)) return;
|
|
78
|
+
webRef.current?.injectJavaScript(centerCommandScript(value));
|
|
79
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
|
+
}, [value?.lat, value?.lng, ready]);
|
|
81
|
+
|
|
82
|
+
if (failed || !WebView || !src) {
|
|
83
|
+
return <MapPinPicker value={value} onChange={onChange} defaultCenter={defaultCenter} defaultZoom={defaultZoom} height={height} cornerRadius={cornerRadius} />;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<View style={{ height, borderRadius: cornerRadius ?? radius.md, overflow: 'hidden', backgroundColor: colors.backgroundSecondary }}>
|
|
88
|
+
<WebView
|
|
89
|
+
ref={webRef}
|
|
90
|
+
source={{ uri: src }}
|
|
91
|
+
style={{ flex: 1, backgroundColor: 'transparent' }}
|
|
92
|
+
javaScriptEnabled
|
|
93
|
+
// Android: this WebView sits inside StickyActions' ScrollView, and a
|
|
94
|
+
// ScrollView intercepts every vertical drag its children start, so the
|
|
95
|
+
// map moved a few pixels and stopped while the page scrolled instead.
|
|
96
|
+
// nestedScrollEnabled makes the WebView ask the parent not to intercept
|
|
97
|
+
// (requestDisallowInterceptTouchEvent) for the life of the touch. No-op
|
|
98
|
+
// on iOS. Reported on a Galaxy S24, 2026-09-07.
|
|
99
|
+
nestedScrollEnabled
|
|
100
|
+
onError={() => setFailed(true)}
|
|
101
|
+
onMessage={(event) => {
|
|
102
|
+
const msg = parseMapFrameMessage(event.nativeEvent.data);
|
|
103
|
+
if (!msg) return;
|
|
104
|
+
if (msg.type === 'ready') {
|
|
105
|
+
readyRef.current = true;
|
|
106
|
+
setReady(true);
|
|
107
|
+
} else if (msg.type === 'failed') {
|
|
108
|
+
setFailed(true);
|
|
109
|
+
} else {
|
|
110
|
+
lastFromFrame.current = { lat: msg.lat, lng: msg.lng };
|
|
111
|
+
onChangeRef.current({ lat: msg.lat, lng: msg.lng });
|
|
112
|
+
}
|
|
113
|
+
}}
|
|
114
|
+
/>
|
|
115
|
+
{!ready ? (
|
|
116
|
+
<View pointerEvents="none" style={{ position: 'absolute', top: 0, right: 0, bottom: 0, left: 0, alignItems: 'center', justifyContent: 'center' }}>
|
|
117
|
+
<ActivityIndicator size="small" color={colors.primary} />
|
|
118
|
+
</View>
|
|
119
|
+
) : null}
|
|
120
|
+
</View>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius, spacing } from '../config/theme';
|
|
5
|
+
import { useTheme } from './runtime';
|
|
6
|
+
import { MyazaText } from './Typography';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The "Your location" pill beside a pinned geo row: primary tint, primary
|
|
10
|
+
* text. It marks a GUESS made on the visitor's behalf, never a verdict, and
|
|
11
|
+
* the row it sits on is one tap away rather than buried in the alphabet.
|
|
12
|
+
* Shared by the country-select picker and the dial-code sheet so the tag
|
|
13
|
+
* reads identically wherever the guess is offered.
|
|
14
|
+
*/
|
|
15
|
+
export function GeoBadge({ label }: { label: string }): React.ReactElement {
|
|
16
|
+
const { colors } = useTheme();
|
|
17
|
+
return (
|
|
18
|
+
<View
|
|
19
|
+
style={{
|
|
20
|
+
marginRight: spacing.sm,
|
|
21
|
+
paddingHorizontal: spacing.sm,
|
|
22
|
+
paddingVertical: 2,
|
|
23
|
+
borderRadius: radius.full,
|
|
24
|
+
borderWidth: 1,
|
|
25
|
+
borderColor: colors.primary,
|
|
26
|
+
backgroundColor: colors.primary50,
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '500' }}>
|
|
30
|
+
{label}
|
|
31
|
+
</MyazaText>
|
|
32
|
+
</View>
|
|
33
|
+
);
|
|
34
|
+
}
|
package/src/components/Icon.tsx
CHANGED
|
@@ -1,183 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import Ionicons from '@expo/vector-icons/Ionicons';
|
|
3
|
-
import {
|
|
4
|
-
BadgeCheck,
|
|
5
|
-
BookUser,
|
|
6
|
-
Building2,
|
|
7
|
-
Calendar,
|
|
8
|
-
Camera,
|
|
9
|
-
CameraOff,
|
|
10
|
-
ChevronDown,
|
|
11
|
-
ChevronLeft,
|
|
12
|
-
ChevronRight,
|
|
13
|
-
Car,
|
|
14
|
-
Check,
|
|
15
|
-
CircleAlert,
|
|
16
|
-
Contact,
|
|
17
|
-
CreditCard,
|
|
18
|
-
Fingerprint,
|
|
19
|
-
FileText,
|
|
20
|
-
FlaskConical,
|
|
21
|
-
IdCard,
|
|
22
|
-
Image as ImageIcon,
|
|
23
|
-
Landmark,
|
|
24
|
-
Lightbulb,
|
|
25
|
-
Globe,
|
|
26
|
-
Mail,
|
|
27
|
-
Maximize2,
|
|
28
|
-
Lock,
|
|
29
|
-
MessageCircle,
|
|
30
|
-
MessageSquare,
|
|
31
|
-
Moon,
|
|
32
|
-
MoveLeft,
|
|
33
|
-
Nfc,
|
|
34
|
-
Pencil,
|
|
35
|
-
Smartphone,
|
|
36
|
-
RefreshCw,
|
|
37
|
-
ScanFace,
|
|
38
|
-
ScanLine,
|
|
39
|
-
Search,
|
|
40
|
-
Zap,
|
|
41
|
-
ZapOff,
|
|
42
|
-
ShieldCheck,
|
|
43
|
-
Sun,
|
|
44
|
-
Timer,
|
|
45
|
-
Upload,
|
|
46
|
-
UserRound,
|
|
47
|
-
UserRoundPlus,
|
|
48
|
-
Link2,
|
|
49
|
-
Copy,
|
|
50
|
-
Share2,
|
|
51
|
-
UsersRound,
|
|
52
|
-
VideoOff,
|
|
53
|
-
Vote,
|
|
54
|
-
X,
|
|
55
|
-
type LucideIcon,
|
|
56
|
-
} from 'lucide-react-native';
|
|
57
3
|
|
|
58
|
-
|
|
59
|
-
// (`lucide_icons_flutter`) and the web SDK uses (`lucide-react`). Rendered via
|
|
60
|
-
// lucide-react-native (react-native-svg), so the glyphs are pixel-identical
|
|
61
|
-
// across all three platforms.
|
|
4
|
+
import { ICONS, type IconName } from './icon-map';
|
|
62
5
|
|
|
63
|
-
export type IconName
|
|
64
|
-
// chrome
|
|
65
|
-
| 'close'
|
|
66
|
-
| 'back'
|
|
67
|
-
| 'moon'
|
|
68
|
-
| 'sun'
|
|
69
|
-
| 'check'
|
|
70
|
-
| 'lock'
|
|
71
|
-
| 'alert'
|
|
72
|
-
| 'flask'
|
|
73
|
-
| 'refresh'
|
|
74
|
-
| 'maximize'
|
|
75
|
-
| 'calendar'
|
|
76
|
-
| 'image'
|
|
77
|
-
| 'chevron-left'
|
|
78
|
-
| 'chevron-right'
|
|
79
|
-
| 'chevron-down'
|
|
80
|
-
// consent hero + process steps
|
|
81
|
-
| 'shield'
|
|
82
|
-
| 'badge-check'
|
|
83
|
-
| 'building-2'
|
|
84
|
-
| 'user'
|
|
85
|
-
| 'user-plus'
|
|
86
|
-
| 'users'
|
|
87
|
-
| 'scan-line'
|
|
88
|
-
| 'scan-face'
|
|
89
|
-
| 'nfc'
|
|
90
|
-
| 'pencil'
|
|
91
|
-
| 'search'
|
|
92
|
-
| 'zap'
|
|
93
|
-
| 'link'
|
|
94
|
-
| 'copy'
|
|
95
|
-
| 'share'
|
|
96
|
-
| 'zap-off'
|
|
97
|
-
| 'timer'
|
|
98
|
-
// id-type glyphs
|
|
99
|
-
| 'fingerprint'
|
|
100
|
-
| 'credit-card'
|
|
101
|
-
| 'globe'
|
|
102
|
-
| 'car'
|
|
103
|
-
| 'vote'
|
|
104
|
-
| 'file-text'
|
|
105
|
-
| 'landmark'
|
|
106
|
-
| 'id-card'
|
|
107
|
-
| 'contact'
|
|
108
|
-
| 'passport'
|
|
109
|
-
| 'camera'
|
|
110
|
-
| 'camera-off'
|
|
111
|
-
| 'video-off'
|
|
112
|
-
| 'upload'
|
|
113
|
-
| 'x'
|
|
114
|
-
| 'lightbulb'
|
|
115
|
-
// contact verification (OTP channels + step footer)
|
|
116
|
-
| 'mail'
|
|
117
|
-
| 'smartphone'
|
|
118
|
-
| 'message-square'
|
|
119
|
-
| 'message-circle';
|
|
120
|
-
|
|
121
|
-
// Maps each name to its Lucide component. Names mirror the Flutter SDK exactly:
|
|
122
|
-
// shieldCheck · badgeCheck · userRound · scanLine · scanFace · fingerprint ·
|
|
123
|
-
// creditCard · globe · car · vote · fileText (+ chrome: X / ArrowLeft /
|
|
124
|
-
// Moon / Sun / Check / Lock / CircleAlert / RefreshCw / Camera).
|
|
125
|
-
const ICONS: Record<IconName, LucideIcon> = {
|
|
126
|
-
close: X,
|
|
127
|
-
back: MoveLeft, // longer-shaft left arrow, matching Flutter's keyboard_backspace
|
|
128
|
-
moon: Moon,
|
|
129
|
-
sun: Sun,
|
|
130
|
-
check: Check,
|
|
131
|
-
lock: Lock,
|
|
132
|
-
alert: CircleAlert,
|
|
133
|
-
// The environment banner's mark, shared with the web SDK (same lucide glyph)
|
|
134
|
-
// and mirrored by Flutter's Icons.science_outlined.
|
|
135
|
-
flask: FlaskConical,
|
|
136
|
-
refresh: RefreshCw,
|
|
137
|
-
maximize: Maximize2,
|
|
138
|
-
calendar: Calendar,
|
|
139
|
-
image: ImageIcon,
|
|
140
|
-
'chevron-left': ChevronLeft,
|
|
141
|
-
'chevron-right': ChevronRight,
|
|
142
|
-
'chevron-down': ChevronDown,
|
|
143
|
-
shield: ShieldCheck,
|
|
144
|
-
'badge-check': BadgeCheck,
|
|
145
|
-
'building-2': Building2,
|
|
146
|
-
user: UserRound,
|
|
147
|
-
'user-plus': UserRoundPlus,
|
|
148
|
-
link: Link2,
|
|
149
|
-
copy: Copy,
|
|
150
|
-
share: Share2,
|
|
151
|
-
users: UsersRound,
|
|
152
|
-
'scan-line': ScanLine,
|
|
153
|
-
'scan-face': ScanFace,
|
|
154
|
-
nfc: Nfc,
|
|
155
|
-
pencil: Pencil,
|
|
156
|
-
search: Search,
|
|
157
|
-
zap: Zap,
|
|
158
|
-
'zap-off': ZapOff,
|
|
159
|
-
timer: Timer,
|
|
160
|
-
fingerprint: Fingerprint,
|
|
161
|
-
'credit-card': CreditCard,
|
|
162
|
-
globe: Globe,
|
|
163
|
-
car: Car,
|
|
164
|
-
vote: Vote,
|
|
165
|
-
'file-text': FileText,
|
|
166
|
-
landmark: Landmark,
|
|
167
|
-
'id-card': IdCard,
|
|
168
|
-
contact: Contact,
|
|
169
|
-
passport: BookUser,
|
|
170
|
-
camera: Camera,
|
|
171
|
-
'camera-off': CameraOff,
|
|
172
|
-
'video-off': VideoOff,
|
|
173
|
-
upload: Upload,
|
|
174
|
-
x: X,
|
|
175
|
-
lightbulb: Lightbulb,
|
|
176
|
-
mail: Mail,
|
|
177
|
-
smartphone: Smartphone,
|
|
178
|
-
'message-square': MessageSquare,
|
|
179
|
-
'message-circle': MessageCircle,
|
|
180
|
-
};
|
|
6
|
+
export type { IconName } from './icon-map';
|
|
181
7
|
|
|
182
8
|
export interface IconProps {
|
|
183
9
|
name: IconName;
|