@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.
Files changed (246) hide show
  1. package/README.md +151 -5
  2. package/android/build.gradle +35 -3
  3. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +77 -0
  4. package/app.plugin.js +89 -8
  5. package/ios/HybridMyazaFaceDetector.swift +16 -3
  6. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
  7. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
  8. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
  9. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
  10. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
  11. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
  12. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  13. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
  14. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
  15. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
  16. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
  17. package/package.json +14 -2
  18. package/src/MyazaBiometricAuth.tsx +119 -0
  19. package/src/MyazaKYC.tsx +12 -0
  20. package/src/components/CountryField.tsx +8 -0
  21. package/src/components/CountryRegionPicker.tsx +184 -0
  22. package/src/components/DashedBorder.tsx +55 -0
  23. package/src/components/DialCodePicker.tsx +53 -67
  24. package/src/components/DialCodeRow.tsx +102 -0
  25. package/src/components/FramedMapPicker.tsx +122 -0
  26. package/src/components/GeoBadge.tsx +34 -0
  27. package/src/components/Icon.tsx +2 -176
  28. package/src/components/KycFlow.tsx +60 -32
  29. package/src/components/LineSkeleton.tsx +100 -0
  30. package/src/components/MapChrome.tsx +67 -0
  31. package/src/components/MapPinMarker.tsx +54 -0
  32. package/src/components/MapPinPicker.tsx +179 -0
  33. package/src/components/MilestoneTrack.tsx +155 -0
  34. package/src/components/MultiIdProgress.tsx +107 -0
  35. package/src/components/MyazaInput.tsx +39 -4
  36. package/src/components/MyazaSelect.tsx +2 -1
  37. package/src/components/OptionRow.tsx +22 -3
  38. package/src/components/OwnershipSlider.tsx +102 -0
  39. package/src/components/PhoneNumberInput.tsx +45 -4
  40. package/src/components/PoweredBy.tsx +5 -5
  41. package/src/components/PresenceBlocks.tsx +153 -0
  42. package/src/components/StaggerIn.tsx +63 -0
  43. package/src/components/StepView.tsx +43 -0
  44. package/src/components/StickyActions.tsx +56 -0
  45. package/src/components/dialCodeRows.ts +64 -0
  46. package/src/components/glass/FloatingSheet.tsx +73 -21
  47. package/src/components/glass/GlassSheet.tsx +14 -2
  48. package/src/components/icon-map.ts +176 -0
  49. package/src/components/stepHeaderMeta.tsx +45 -5
  50. package/src/config/addressCollection.ts +125 -0
  51. package/src/config/biometricOptions.ts +100 -0
  52. package/src/config/business.ts +26 -7
  53. package/src/config/businessDetailsValidity.ts +52 -0
  54. package/src/config/businessPrefill.ts +72 -0
  55. package/src/config/businessSteps.ts +20 -3
  56. package/src/config/consentStep.ts +19 -0
  57. package/src/config/keyPeople.ts +111 -7
  58. package/src/config/keyPeopleCorporate.ts +19 -0
  59. package/src/config/keyPeopleOwnerTypes.ts +12 -0
  60. package/src/config/keyPeopleOwners.ts +26 -0
  61. package/src/config/keyPeoplePrefill.ts +138 -0
  62. package/src/config/keyPeopleSectionDefs.ts +65 -0
  63. package/src/config/keyPeopleSections.ts +167 -0
  64. package/src/config/proofOfAddress.ts +54 -11
  65. package/src/config/regions.ts +24 -0
  66. package/src/config/registrationHint.ts +1 -1
  67. package/src/config/stepOrder.ts +100 -6
  68. package/src/config/theme.ts +8 -2
  69. package/src/config/uploadLimits.ts +35 -0
  70. package/src/config/website.ts +36 -0
  71. package/src/config/workflowMerge.ts +16 -5
  72. package/src/emrtd/activeAuth.ts +100 -0
  73. package/src/emrtd/extras.ts +76 -0
  74. package/src/emrtd/files.ts +5 -0
  75. package/src/emrtd/index.ts +1 -0
  76. package/src/emrtd/read.ts +8 -1
  77. package/src/emrtd/session.ts +41 -0
  78. package/src/emrtd/stages.ts +11 -0
  79. package/src/index.ts +54 -1
  80. package/src/lib/address-current-location.ts +171 -0
  81. package/src/lib/address-field-modes.ts +119 -0
  82. package/src/lib/address-flow.ts +172 -0
  83. package/src/lib/address-helpers.ts +48 -0
  84. package/src/lib/address-line.ts +76 -0
  85. package/src/lib/address-pin-move.ts +97 -0
  86. package/src/lib/address-step-recovery.ts +63 -0
  87. package/src/lib/authed-image.ts +78 -0
  88. package/src/lib/biometric-auth.ts +52 -0
  89. package/src/lib/biometric-copy.ts +51 -0
  90. package/src/lib/captureRing.ts +83 -0
  91. package/src/lib/contact-recovery.ts +44 -0
  92. package/src/lib/country-adoption.ts +89 -0
  93. package/src/lib/inferred-country.ts +65 -0
  94. package/src/lib/livenessLayout.ts +49 -0
  95. package/src/lib/map-frame.ts +159 -0
  96. package/src/lib/map-tiles.ts +143 -0
  97. package/src/lib/multi-id.ts +210 -0
  98. package/src/lib/poa-country-gate.ts +27 -0
  99. package/src/lib/resubmit.ts +130 -0
  100. package/src/lib/result-copy.ts +116 -0
  101. package/src/lib/result-wait.ts +53 -0
  102. package/src/lib/review-map-surface.ts +26 -0
  103. package/src/lib/scope.ts +31 -0
  104. package/src/lib/screen-corners.ts +50 -0
  105. package/src/lib/selfie-upload-wait.ts +79 -0
  106. package/src/lib/step-log.ts +24 -3
  107. package/src/lib/street-view-fov.ts +42 -0
  108. package/src/lib/use-multi-id-plan.ts +29 -0
  109. package/src/lib/webview-available.ts +47 -0
  110. package/src/liveness/useLiveness.ts +6 -1
  111. package/src/liveness/useModelReady.ts +65 -0
  112. package/src/liveness/visionCameraFaceDetector.ts +35 -0
  113. package/src/presence/background-math.ts +100 -0
  114. package/src/presence/background-store.ts +82 -0
  115. package/src/presence/background.ts +165 -0
  116. package/src/presence/foreground-service.ts +193 -0
  117. package/src/presence/fs.ts +51 -0
  118. package/src/presence/geofence.ts +41 -0
  119. package/src/presence/math.ts +44 -0
  120. package/src/presence/post.ts +40 -0
  121. package/src/presence/report.ts +87 -0
  122. package/src/presence/sampler.ts +110 -0
  123. package/src/presence/status.ts +92 -0
  124. package/src/presence/store.ts +94 -0
  125. package/src/presence/tier.ts +36 -0
  126. package/src/presence/watch-wait.ts +81 -0
  127. package/src/screens/AddressCountryControl.tsx +115 -0
  128. package/src/screens/ApplicantRoleStep.tsx +9 -5
  129. package/src/screens/BusinessCheckPanel.tsx +89 -0
  130. package/src/screens/BusinessDetailsFields.tsx +112 -0
  131. package/src/screens/BusinessDetailsStep.tsx +133 -145
  132. package/src/screens/BusinessDocumentSlot.tsx +5 -5
  133. package/src/screens/BusinessDocumentsStep.tsx +5 -56
  134. package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
  135. package/src/screens/BusinessPickedCard.tsx +86 -0
  136. package/src/screens/BusinessPickedSection.tsx +51 -0
  137. package/src/screens/BusinessRegistryPickers.tsx +67 -0
  138. package/src/screens/BusinessSandboxToggle.tsx +114 -0
  139. package/src/screens/BusinessSearch.tsx +199 -0
  140. package/src/screens/BusinessSearchResults.tsx +115 -0
  141. package/src/screens/CompanyInfoFields.tsx +91 -51
  142. package/src/screens/ConsentStep.tsx +37 -17
  143. package/src/screens/ContactActions.tsx +54 -0
  144. package/src/screens/ContactDestinationField.tsx +5 -0
  145. package/src/screens/ContactEntryPanel.tsx +75 -0
  146. package/src/screens/ContactVerificationStep.tsx +43 -42
  147. package/src/screens/CountrySelectStep.tsx +13 -147
  148. package/src/screens/IdTypeStep.tsx +12 -5
  149. package/src/screens/KeepLinksSheet.tsx +88 -0
  150. package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
  151. package/src/screens/KeyPeopleAwaitList.tsx +126 -0
  152. package/src/screens/KeyPeoplePending.tsx +154 -0
  153. package/src/screens/KeyPeopleSection.tsx +162 -0
  154. package/src/screens/KeyPeopleSectionsList.tsx +103 -0
  155. package/src/screens/KeyPeopleUboExemption.tsx +72 -0
  156. package/src/screens/KeyPersonCard.tsx +36 -4
  157. package/src/screens/KeyPersonForm.tsx +195 -36
  158. package/src/screens/KeyPersonKindToggle.tsx +68 -0
  159. package/src/screens/KeyPersonOwners.tsx +109 -0
  160. package/src/screens/KeyPersonRoleChips.tsx +68 -0
  161. package/src/screens/KeyPersonSheet.tsx +92 -108
  162. package/src/screens/LivenessAvatar.tsx +15 -3
  163. package/src/screens/LivenessStep.tsx +172 -52
  164. package/src/screens/NfcStep.tsx +14 -0
  165. package/src/screens/PoaDocumentTypeList.tsx +66 -0
  166. package/src/screens/ProofOfAddressParts.tsx +151 -0
  167. package/src/screens/ProofOfAddressStep.tsx +58 -96
  168. package/src/screens/SubmittedBadge.tsx +25 -0
  169. package/src/screens/SubmittedError.tsx +64 -0
  170. package/src/screens/SubmittedResult.tsx +117 -0
  171. package/src/screens/SubmittedStep.tsx +82 -119
  172. package/src/screens/SubmittedSuccess.tsx +127 -0
  173. package/src/screens/SubmittedWaiting.tsx +45 -0
  174. package/src/screens/address/AddressEntranceStep.tsx +161 -0
  175. package/src/screens/address/AddressIntroGate.tsx +148 -0
  176. package/src/screens/address/AddressMapStub.tsx +50 -0
  177. package/src/screens/address/AddressPinStep.tsx +178 -0
  178. package/src/screens/address/AddressReviewStep.tsx +162 -0
  179. package/src/screens/address/AddressSandboxTabs.tsx +158 -0
  180. package/src/screens/address/AddressSearchField.tsx +115 -0
  181. package/src/screens/address/AddressSearchStep.tsx +94 -0
  182. package/src/screens/address/CurrentLocationRow.tsx +135 -0
  183. package/src/screens/address/DetailsSheet.tsx +175 -0
  184. package/src/screens/address/DetailsSheetFields.tsx +184 -0
  185. package/src/screens/address/EntranceDropzone.tsx +178 -0
  186. package/src/screens/address/EntranceFraming.tsx +66 -0
  187. package/src/screens/address/EntrancePills.tsx +60 -0
  188. package/src/screens/address/FramedStreetView.tsx +155 -0
  189. package/src/screens/address/IntroDisclosures.tsx +210 -0
  190. package/src/screens/address/LabelDecisionRow.tsx +113 -0
  191. package/src/screens/address/PinSummaryRow.tsx +105 -0
  192. package/src/screens/address/ReviewAddressBand.tsx +111 -0
  193. package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
  194. package/src/screens/address/ReviewMapPicture.tsx +106 -0
  195. package/src/screens/address/SearchResults.tsx +105 -0
  196. package/src/screens/address/SearchScreen.tsx +176 -0
  197. package/src/screens/address/SkipForNow.tsx +43 -0
  198. package/src/screens/address/StreetViewChrome.tsx +81 -0
  199. package/src/screens/address/detail-values.ts +22 -0
  200. package/src/screens/address/fix-source.ts +27 -0
  201. package/src/screens/address/index.ts +8 -0
  202. package/src/screens/address/meta.ts +44 -0
  203. package/src/screens/address/use-address-flow.ts +200 -0
  204. package/src/screens/address/use-label-pin.ts +80 -0
  205. package/src/screens/address/use-pin-actions.ts +192 -0
  206. package/src/screens/biometric/BiometricAuthFlow.tsx +170 -0
  207. package/src/screens/consent/model.ts +79 -10
  208. package/src/screens/liveness/CaptureRing.tsx +91 -0
  209. package/src/screens/liveness/LivenessCamera.tsx +63 -0
  210. package/src/screens/liveness/LivenessHandover.tsx +43 -0
  211. package/src/screens/liveness/LivenessOutcome.tsx +4 -3
  212. package/src/screens/liveness/SelfiePreview.tsx +13 -4
  213. package/src/screens/liveness/ShutterFlash.tsx +25 -0
  214. package/src/screens/liveness/index.ts +4 -0
  215. package/src/screens/liveness/useSelfieUpload.ts +23 -3
  216. package/src/screens/nfc/NfcReadProgress.tsx +1 -0
  217. package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
  218. package/src/screens/useAddressPhotoAttach.ts +77 -0
  219. package/src/screens/useAwaitingPeople.ts +90 -0
  220. package/src/screens/useBusinessDocumentAttach.ts +93 -0
  221. package/src/screens/usePoaAttach.ts +12 -15
  222. package/src/services/api-biometric.ts +37 -0
  223. package/src/services/api-types-biometric.ts +41 -0
  224. package/src/services/api-types.ts +251 -3
  225. package/src/services/api-verify-types.ts +90 -1
  226. package/src/services/api.ts +237 -3
  227. package/src/services/deviceMetadata.ts +1 -1
  228. package/src/services/errors.ts +10 -2
  229. package/src/services/location.ts +234 -0
  230. package/src/services/uploadErrors.ts +30 -0
  231. package/src/services/workflowGate.ts +8 -0
  232. package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
  233. package/src/store/address.ts +97 -0
  234. package/src/store/businessCheck.ts +113 -0
  235. package/src/store/derive.ts +44 -4
  236. package/src/store/kycStore.ts +352 -16
  237. package/src/store/serverConfig.ts +20 -0
  238. package/src/store/session.ts +227 -0
  239. package/src/store/state.ts +289 -2
  240. package/src/store/submit.ts +80 -13
  241. package/src/store/submitApplicant.ts +5 -11
  242. package/src/types/business.ts +23 -2
  243. package/src/types/config.ts +70 -2
  244. package/src/types/verification.ts +13 -1
  245. package/src/types/workflow.ts +112 -1
  246. 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
- const filtered = useMemo(() => {
82
- const q = query.trim().toLowerCase();
83
- if (!q) return options;
84
- // Match the name, the ISO code, or the dial code with or without its '+',
85
- // because people search for "+234", "234" and "Nigeria" in equal measure.
86
- const bare = q.replace(/^\+/, '');
87
- return options.filter(
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
- <View style={{ paddingHorizontal: spacing.md, paddingBottom: spacing.sm }}>
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={filtered}
121
- keyExtractor={(o) => o.code}
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
- <CountryRow
130
- option={item}
131
- isSelected={item.code === selected}
132
- onPress={() => {
133
- setQuery('');
134
- onPick(item.code);
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
+ }
@@ -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
- // Centralised icon set — the SAME Lucide icons the Flutter SDK uses
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;