@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
@@ -0,0 +1,158 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Animated, Easing, Pressable, View } from 'react-native';
3
+
4
+ import { MyazaText } from '../../components/Typography';
5
+ import { Icon, type IconName } from '../../components/Icon';
6
+ import { useKyc, useKycStore, useTheme } from '../../components/runtime';
7
+ import { DashedBorder } from '../../components/DashedBorder';
8
+ import { spacing } from '../../config/theme';
9
+
10
+ // Dev/sandbox only: the address flow's Test-result tabs — the web SDK's
11
+ // control, on the BusinessSandboxToggle's mechanics. Four equal columns share
12
+ // one sliding indicator whose FILL takes the active outcome's semantic colour
13
+ // (the dashboard's tier/verdict language), so moving from Attested to
14
+ // Mismatch reads as one pill travelling and turning red. Tabs are icon-only
15
+ // (four labels never fit a phone) and the caption names the active pick.
16
+ // Nothing is SENT until the operator taps; unclicked keeps the server default,
17
+ // which the resting position mirrors.
18
+
19
+ type Outcome =
20
+ | 'address_attested'
21
+ | 'address_corroborated'
22
+ | 'address_collected'
23
+ | 'address_mismatch';
24
+
25
+ // `tint` is the icon's own colour while the tab is INACTIVE, so the row reads
26
+ // as four outcomes rather than one live state and three dead ones. The pairs
27
+ // are web's `text-emerald-600 dark:text-emerald-400` and friends, hex for hex;
28
+ // `null` takes the theme's muted tone, web's `text-muted-foreground`.
29
+ const OPTIONS: Array<{
30
+ key: Outcome;
31
+ label: string;
32
+ icon: IconName;
33
+ pill: string;
34
+ tint: { light: string; dark: string } | null;
35
+ }> = [
36
+ {
37
+ key: 'address_attested',
38
+ label: 'Attested',
39
+ icon: 'shield',
40
+ pill: '#059669',
41
+ tint: { light: '#059669', dark: '#34d399' },
42
+ },
43
+ {
44
+ key: 'address_corroborated',
45
+ label: 'Corroborated',
46
+ icon: 'badge-check',
47
+ pill: '#0284c7',
48
+ tint: { light: '#0284c7', dark: '#38bdf8' },
49
+ },
50
+ {
51
+ key: 'address_collected',
52
+ label: 'Collected',
53
+ icon: 'circle-dashed',
54
+ pill: '#475569',
55
+ tint: null,
56
+ },
57
+ {
58
+ key: 'address_mismatch',
59
+ label: 'Mismatch',
60
+ icon: 'circle-x',
61
+ pill: '#dc2626',
62
+ tint: { light: '#dc2626', dark: '#f87171' },
63
+ },
64
+ ];
65
+
66
+ export function AddressSandboxTabs(): React.ReactElement | null {
67
+ const store = useKycStore();
68
+ const { colors, mode } = useTheme();
69
+ const environment = useKyc((s) => s.serverConfig.environment);
70
+ const picked = useKyc((s) => s.addressSandboxOutcome);
71
+ const shown = picked ?? 'address_attested';
72
+ const index = Math.max(0, OPTIONS.findIndex((o) => o.key === shown));
73
+ const [trackWidth, setTrackWidth] = useState(0);
74
+ // Border box minus borders and padding, quartered — the business toggle's
75
+ // measurement rule, for four columns.
76
+ const indicatorWidth = Math.max(0, (trackWidth - 2 * 1 - 2 * 2) / 4);
77
+ const slide = useRef(new Animated.Value(index)).current;
78
+
79
+ useEffect(() => {
80
+ Animated.timing(slide, {
81
+ toValue: index,
82
+ duration: 200,
83
+ easing: Easing.out(Easing.quad),
84
+ useNativeDriver: true,
85
+ }).start();
86
+ }, [index, slide]);
87
+
88
+ if (environment === 'PRODUCTION') return null;
89
+
90
+ return (
91
+ <View style={{ borderRadius: 12, padding: 12 }}>
92
+ <DashedBorder color={colors.border} radius={12} strokeWidth={1} />
93
+ <MyazaText variant="label">Test result</MyazaText>
94
+ <View style={{ height: spacing.sm }} />
95
+ <View
96
+ onLayout={(e) => setTrackWidth(e.nativeEvent.layout.width)}
97
+ style={{
98
+ flexDirection: 'row',
99
+ borderWidth: 1,
100
+ borderColor: colors.border,
101
+ borderRadius: 8,
102
+ padding: 2,
103
+ }}
104
+ >
105
+ {trackWidth > 0 ? (
106
+ <Animated.View
107
+ pointerEvents="none"
108
+ style={{
109
+ position: 'absolute',
110
+ top: 2,
111
+ bottom: 2,
112
+ left: 2,
113
+ width: indicatorWidth,
114
+ borderRadius: 6,
115
+ backgroundColor: OPTIONS[index]!.pill,
116
+ transform: [
117
+ {
118
+ translateX: slide.interpolate({
119
+ inputRange: [0, OPTIONS.length - 1],
120
+ outputRange: [0, indicatorWidth * (OPTIONS.length - 1)],
121
+ }),
122
+ },
123
+ ],
124
+ }}
125
+ />
126
+ ) : null}
127
+ {OPTIONS.map(({ key, label, icon, tint }, i) => {
128
+ const active = index === i;
129
+ return (
130
+ <Pressable
131
+ key={key}
132
+ accessibilityRole="button"
133
+ accessibilityLabel={label}
134
+ accessibilityState={{ selected: active }}
135
+ onPress={() => store.getState().setAddressSandboxOutcome(key)}
136
+ style={{ flex: 1, alignItems: 'center', justifyContent: 'center', height: 40 }}
137
+ >
138
+ {/* Above the indicator, or the icon slides out from under it. */}
139
+ <Icon
140
+ name={icon}
141
+ size={18}
142
+ color={
143
+ active
144
+ ? '#ffffff'
145
+ : (tint ? tint[mode === 'dark' ? 'dark' : 'light'] : colors.textSecondary)
146
+ }
147
+ />
148
+ </Pressable>
149
+ );
150
+ })}
151
+ </View>
152
+ <View style={{ height: spacing.sm }} />
153
+ <MyazaText variant="bodySmall" color={colors.textSecondary}>
154
+ {OPTIONS[index]!.label} is returned instead of judging the pin.
155
+ </MyazaText>
156
+ </View>
157
+ );
158
+ }
@@ -0,0 +1,115 @@
1
+ import React from 'react';
2
+ import { ActivityIndicator, Pressable, TextInput, View } from 'react-native';
3
+
4
+ import { radius, sizing, spacing } from '../../config/theme';
5
+ import { useTheme } from '../../components/runtime';
6
+ import { useInputFontFamily } from '../../components/fonts';
7
+ import { Icon } from '../../components/Icon';
8
+
9
+ /**
10
+ * The address search box.
11
+ *
12
+ * Its own field rather than MyazaInput because it needs the return key to
13
+ * submit (the basic backend is explicit-submit only) and a trailing spinner
14
+ * while a details call is out — neither of which belongs on the generic input.
15
+ * The border, radius and height are the SAME tokens, so it reads as one family.
16
+ */
17
+ export function AddressSearchField({
18
+ value,
19
+ onChangeText,
20
+ busy,
21
+ showSubmit,
22
+ canSubmit,
23
+ autoFocus,
24
+ onSubmit,
25
+ }: {
26
+ value: string;
27
+ onChangeText: (text: string) => void;
28
+ /** A request is in flight: a details call (autocomplete) or the search. */
29
+ busy: boolean;
30
+ /** The basic backend gets an explicit Search button beside the field. */
31
+ showSubmit: boolean;
32
+ canSubmit: boolean;
33
+ autoFocus: boolean;
34
+ onSubmit: () => void;
35
+ }): React.ReactElement {
36
+ const { colors } = useTheme();
37
+ const fontFamily = useInputFontFamily();
38
+
39
+ return (
40
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: spacing.sm }}>
41
+ <View
42
+ style={{
43
+ flex: 1,
44
+ flexDirection: 'row',
45
+ alignItems: 'center',
46
+ height: sizing.inputHeight,
47
+ paddingHorizontal: spacing.md,
48
+ borderWidth: 1,
49
+ borderColor: colors.border,
50
+ borderRadius: radius.sm,
51
+ backgroundColor: colors.background,
52
+ }}
53
+ >
54
+ <Icon name="search" size={16} color={colors.textSecondary} />
55
+ <TextInput
56
+ value={value}
57
+ onChangeText={onChangeText}
58
+ placeholder="Search your address, e.g. 12 Adeola Odeku Street"
59
+ placeholderTextColor={colors.textMuted}
60
+ accessibilityLabel="Search your address"
61
+ autoCapitalize="none"
62
+ // An address is a proper noun: iOS autocorrect rewrites street names
63
+ // right before the person submits.
64
+ autoCorrect={false}
65
+ autoFocus={autoFocus}
66
+ returnKeyType="search"
67
+ onSubmitEditing={() => {
68
+ if (canSubmit) onSubmit();
69
+ }}
70
+ style={{
71
+ flex: 1,
72
+ height: '100%',
73
+ marginLeft: spacing.sm,
74
+ color: colors.textDark,
75
+ fontSize: 16,
76
+ fontFamily,
77
+ }}
78
+ />
79
+ {busy && !showSubmit ? <ActivityIndicator size="small" color={colors.textSecondary} /> : null}
80
+ </View>
81
+
82
+ {showSubmit ? (
83
+ <Pressable
84
+ onPress={canSubmit ? onSubmit : undefined}
85
+ disabled={!canSubmit}
86
+ accessibilityRole="button"
87
+ accessibilityLabel="Search"
88
+ // Web's OUTLINE button (`variant="outline" h-11 w-11 rounded-xl`),
89
+ // which Flutter draws too: a bordered box whose glyph takes the
90
+ // primary once the query is long enough, never a filled button.
91
+ style={{
92
+ width: sizing.inputHeight,
93
+ height: sizing.inputHeight,
94
+ borderRadius: radius.sm,
95
+ alignItems: 'center',
96
+ justifyContent: 'center',
97
+ borderWidth: 1,
98
+ borderColor: colors.border,
99
+ backgroundColor: colors.background,
100
+ }}
101
+ >
102
+ {busy ? (
103
+ <ActivityIndicator size="small" color={colors.primary} />
104
+ ) : (
105
+ <Icon
106
+ name="search"
107
+ size={18}
108
+ color={canSubmit ? colors.primary : colors.textMuted}
109
+ />
110
+ )}
111
+ </Pressable>
112
+ ) : null}
113
+ </View>
114
+ );
115
+ }
@@ -0,0 +1,94 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../../config/theme';
5
+ import { useKyc, useKycStore, useTheme } from '../../components/runtime';
6
+ import { MyazaText } from '../../components/Typography';
7
+ import { pickedAddressState } from '../../lib/address-helpers';
8
+ import { useAddressFlow } from './use-address-flow';
9
+ import { useAddressIntroGate } from './AddressIntroGate';
10
+ import { SearchScreen } from './SearchScreen';
11
+ import { SkipForNow } from './SkipForNow';
12
+
13
+ /**
14
+ * Step 1 of the address flow: find the address as words. Every path — a picked
15
+ * suggestion, the current location, or "place a pin instead" — lands on the pin
16
+ * step.
17
+ */
18
+ export function AddressSearchStep(): React.ReactElement {
19
+ const store = useKycStore();
20
+ const { colors } = useTheme();
21
+ const flow = useAddressFlow();
22
+ const autocomplete = useKyc((s) => s.serverConfig.addressSearchMode === 'autocomplete');
23
+ const gate = useAddressIntroGate('address-search', flow.steps[0]!);
24
+
25
+ // Warm the GPS and its reverse geocode from the moment the flow is reached,
26
+ // UNDER the primer too: by the time "Got it" is tapped the fix is usually
27
+ // already resolved, so the row carries the address immediately and the pin
28
+ // lands with no hesitation.
29
+ const { startPrefetch } = flow;
30
+ useEffect(() => {
31
+ startPrefetch();
32
+ }, [startPrefetch]);
33
+
34
+ if (gate) return gate;
35
+
36
+ const toPin = (): void => store.getState().goToStep('address-collection');
37
+
38
+ return (
39
+ <View>
40
+ <SearchScreen
41
+ country={flow.country}
42
+ autocomplete={autocomplete}
43
+ near={flow.currentFix ? { lat: flow.currentFix.lat, lng: flow.currentFix.lng } : null}
44
+ locationHint={flow.currentFix?.label ?? null}
45
+ locating={flow.locating}
46
+ onUseMyLocation={() => void flow.applyCurrentFix(toPin)}
47
+ onPinInstead={toPin}
48
+ onResolved={(hit) => {
49
+ // The picked address's own country IS the declaration (a pick is
50
+ // the applicant saying "this is my address"), under the same
51
+ // guess-only / accepted-list rules as every geocode adoption.
52
+ flow.adoptGeocodedCountry(hit.country, { explicit: true });
53
+ const current = store.getState().address;
54
+ store.getState().setAddress({
55
+ ...pickedAddressState(current, hit),
56
+ // `pickedAt` is what makes this a HUMAN-CONFIRMED label: it
57
+ // survives pin nudges instead of being re-derived on every drag.
58
+ // Never set it from a reverse geocode.
59
+ ...(hit.formatted
60
+ ? { label: hit.formatted, pickedAt: { lat: hit.lat, lng: hit.lng } }
61
+ : {}),
62
+ // A pick that RESOLVES a street retires the typed one: that input
63
+ // only existed because no source knew the street, and a hidden
64
+ // field must not keep leading the composed line.
65
+ ...(hit.road ? { street: undefined } : {}),
66
+ // Places picks carry the breakdown; basic hits fall back to the
67
+ // label line in the details sheet.
68
+ ...(hit.road || hit.area || hit.city || hit.state || hit.postcode || hit.country
69
+ ? {
70
+ parts: {
71
+ street: hit.road ?? null,
72
+ area: hit.area ?? null,
73
+ city: hit.city ?? null,
74
+ state: hit.state ?? null,
75
+ postcode: hit.postcode ?? null,
76
+ country: hit.country ?? null,
77
+ },
78
+ }
79
+ : {}),
80
+ });
81
+ toPin();
82
+ }}
83
+ />
84
+
85
+ {flow.error ? (
86
+ <MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.sm }}>
87
+ {flow.error}
88
+ </MyazaText>
89
+ ) : null}
90
+
91
+ <SkipForNow requirePin={flow.cfg?.requirePin} onPress={flow.exitForward} />
92
+ </View>
93
+ );
94
+ }
@@ -0,0 +1,135 @@
1
+ import React from 'react';
2
+ import { ActivityIndicator, Pressable, View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../../config/theme';
5
+ import { useTheme } from '../../components/runtime';
6
+ import { Icon } from '../../components/Icon';
7
+ import { MyazaText } from '../../components/Typography';
8
+
9
+ /**
10
+ * "Use my current location" as a proper row, not a button pretending to be two:
11
+ * a medallion showing the fix's state, the RESOLVED ADDRESS underneath the
12
+ * title — so the person can see where it will take them before tapping — and a
13
+ * chevron that says this goes somewhere.
14
+ *
15
+ * Shared by the search step and by the pin step while no pin exists.
16
+ */
17
+ export function CurrentLocationRow({
18
+ hint,
19
+ locating,
20
+ onPress,
21
+ }: {
22
+ /** The device's resolved current address, when known. */
23
+ hint: string | null;
24
+ locating: boolean;
25
+ onPress: () => void;
26
+ }): React.ReactElement {
27
+ const { colors } = useTheme();
28
+ return (
29
+ <Pressable
30
+ onPress={onPress}
31
+ accessibilityRole="button"
32
+ accessibilityLabel="Use my current location"
33
+ style={({ pressed }) => ({
34
+ flexDirection: 'row',
35
+ alignItems: 'center',
36
+ gap: spacing.md,
37
+ paddingHorizontal: spacing.md - 2,
38
+ paddingVertical: spacing.md - 4,
39
+ borderRadius: radius.md,
40
+ borderWidth: 1,
41
+ borderColor: pressed ? colors.primary : colors.border,
42
+ backgroundColor: pressed ? colors.primary50 : colors.backgroundSecondary,
43
+ })}
44
+ >
45
+ <View
46
+ style={{
47
+ width: 40,
48
+ height: 40,
49
+ borderRadius: radius.full,
50
+ alignItems: 'center',
51
+ justifyContent: 'center',
52
+ backgroundColor: hint ? colors.primary : colors.primary100,
53
+ }}
54
+ >
55
+ {locating && !hint ? (
56
+ <ActivityIndicator size="small" color={colors.primary} />
57
+ ) : (
58
+ <Icon
59
+ name={hint ? 'map-pin' : 'locate'}
60
+ size={18}
61
+ color={hint ? colors.onPrimary : colors.primary}
62
+ />
63
+ )}
64
+ </View>
65
+ <View style={{ flex: 1, minWidth: 0 }}>
66
+ <MyazaText variant="bodyMedium" style={{ fontWeight: '600' }}>
67
+ Use my current location
68
+ </MyazaText>
69
+ <MyazaText variant="bodySmall" color={colors.textSecondary} numberOfLines={1}>
70
+ {hint ?? (locating ? 'Finding your location…' : 'Lands the pin right where you are')}
71
+ </MyazaText>
72
+ </View>
73
+ <Icon name="chevron-right" size={16} color={colors.textSecondary} />
74
+ </Pressable>
75
+ );
76
+ }
77
+
78
+ /**
79
+ * The DEMOTED form of the row above, for once a pin exists. Locating again is
80
+ * then a rare corrective action, so it becomes a labelled chip ON the map,
81
+ * bottom centre — where the eye and thumb already are, and clear of the corners
82
+ * that belong to attribution and the zoom controls.
83
+ *
84
+ * Off the Continue path and deliberate to hit, where the full-width row invited
85
+ * a mistaken tap that yanked a confirmed address to wherever the phone was.
86
+ */
87
+ export function LocateChip({
88
+ locating,
89
+ onPress,
90
+ }: {
91
+ locating: boolean;
92
+ onPress: () => void;
93
+ }): React.ReactElement {
94
+ const { colors } = useTheme();
95
+ return (
96
+ <View
97
+ pointerEvents="box-none"
98
+ style={{ position: 'absolute', left: 0, right: 0, bottom: spacing.md, alignItems: 'center' }}
99
+ >
100
+ <Pressable
101
+ onPress={locating ? undefined : onPress}
102
+ disabled={locating}
103
+ accessibilityRole="button"
104
+ accessibilityLabel="Move the pin to my current location"
105
+ // Web's `h-10 pl-3 pr-3.5 gap-2 shadow-md`, as Flutter draws it.
106
+ style={({ pressed }) => ({
107
+ flexDirection: 'row',
108
+ alignItems: 'center',
109
+ gap: spacing.sm,
110
+ height: 40,
111
+ paddingLeft: spacing.sm + 4,
112
+ paddingRight: spacing.md - 2,
113
+ borderRadius: radius.full,
114
+ borderWidth: 1,
115
+ borderColor: colors.border,
116
+ backgroundColor: pressed ? colors.backgroundSecondary : colors.background,
117
+ shadowColor: colors.textDark,
118
+ shadowOpacity: 0.12,
119
+ shadowRadius: 6,
120
+ shadowOffset: { width: 0, height: 2 },
121
+ elevation: 2,
122
+ })}
123
+ >
124
+ {locating ? (
125
+ <ActivityIndicator size="small" color={colors.textSecondary} />
126
+ ) : (
127
+ <Icon name="locate" size={16} color={colors.primary} />
128
+ )}
129
+ <MyazaText variant="bodySmall" style={{ fontWeight: '600' }}>
130
+ {locating ? 'Finding you…' : 'Use my location'}
131
+ </MyazaText>
132
+ </Pressable>
133
+ </View>
134
+ );
135
+ }
@@ -0,0 +1,175 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Keyboard, Platform, ScrollView, useWindowDimensions, View } from 'react-native';
3
+
4
+ import { spacing } from '../../config/theme';
5
+ import { useTheme } from '../../components/runtime';
6
+ import { MyazaText } from '../../components/Typography';
7
+ import { MyazaButton } from '../../components/MyazaButton';
8
+ import { FloatingSheet } from '../../components/glass/FloatingSheet';
9
+ import { addressFieldModes, type AddressFieldKey } from '../../lib/address-field-modes';
10
+ import type { AddressCollectionConfig } from '../../types/workflow';
11
+ import { AreaFields, EditField, SectionHeading, type DetailPatch, type DetailValues } from './DetailsSheetFields';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // The EDIT-DETAILS sheet, OkHi-style (user decision 2026-08-31): everything on
15
+ // the address is editable. Two grouped sections — street and building, then
16
+ // area and region — with the map's answer prefilling the area fields so the
17
+ // applicant corrects rather than retypes. An untouched prefill is never
18
+ // stored as their claim (the parent only receives what they actually edit).
19
+ //
20
+ // Each typed field honours its workflow mode (lib/address-field-modes.ts):
21
+ // 'off' hides it, 'required' marks it and holds the pin step's Continue.
22
+ // Mirrors the web SDK's DetailsSheet; keep the two in lockstep.
23
+ //
24
+ // Rides the same FloatingSheet every other picker in the SDK uses, and lifts
25
+ // above the keyboard like the key-person sheet, since this one is a form.
26
+ // ---------------------------------------------------------------------------
27
+
28
+ export type DetailsPatch = DetailPatch;
29
+
30
+ export function DetailsSheet({
31
+ isBusiness,
32
+ parts,
33
+ country,
34
+ directionsRequired,
35
+ addressConfig,
36
+ values,
37
+ disabled,
38
+ onChange,
39
+ onClose,
40
+ }: {
41
+ isBusiness: boolean;
42
+ /** The map's answer, broken down — the prefill for the area fields. */
43
+ parts: { street?: string | null; area?: string | null; city?: string | null; state?: string | null; postcode?: string | null } | null;
44
+ /** The flow's ISO-2 country (read-only: a verification fact, not a field). */
45
+ country: string | null;
46
+ directionsRequired: boolean;
47
+ /** The flow's address step: which typed fields are offered or required. */
48
+ addressConfig: AddressCollectionConfig | null | undefined;
49
+ values: DetailValues;
50
+ disabled: boolean;
51
+ onChange: (patch: DetailsPatch) => void;
52
+ onClose: () => void;
53
+ }): React.ReactElement {
54
+ const { colors } = useTheme();
55
+ const { height: screenHeight } = useWindowDimensions();
56
+ const [keyboard, setKeyboard] = useState(0);
57
+
58
+ useEffect(() => {
59
+ const show = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
60
+ const hide = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
61
+ const shown = Keyboard.addListener(show, (e) => setKeyboard(e.endCoordinates.height));
62
+ const hidden = Keyboard.addListener(hide, () => setKeyboard(0));
63
+ return () => {
64
+ shown.remove();
65
+ hidden.remove();
66
+ };
67
+ }, []);
68
+
69
+ const available = screenHeight - keyboard;
70
+ const maxHeight = Math.min(available * 0.92, screenHeight * 0.85);
71
+
72
+ const modes = addressFieldModes(addressConfig);
73
+ const on = (k: AddressFieldKey) => modes[k] !== 'off';
74
+ const req = (k: AddressFieldKey) => modes[k] === 'required';
75
+ const anyRequired = (Object.keys(modes) as AddressFieldKey[]).some(req);
76
+
77
+ // Typed wins (a cleared field STAYS cleared); the map's answer fills the
78
+ // gap only while the applicant has never touched the field.
79
+ const shown = (typed: string | undefined, part: string | null | undefined) =>
80
+ typed ?? part?.trim() ?? '';
81
+
82
+ const twoCol = { flexDirection: 'row' as const, gap: spacing.sm };
83
+ const col = { flex: 1, minWidth: 0 };
84
+ const showStreetSection = on('propertyNumber') || on('street') || on('unit') || on('propertyName');
85
+
86
+ return (
87
+ <FloatingSheet visible onClose={onClose} maxHeight={maxHeight} bottomOffset={keyboard} closeLabel="Close">
88
+ <View style={{ paddingHorizontal: spacing.md, paddingTop: spacing.xs, paddingBottom: spacing.sm }}>
89
+ {/* One close control, not two: FloatingSheet already carries its own
90
+ in the same corner, and the pair read as a mis-render. */}
91
+ <MyazaText variant="body" style={{ fontWeight: '700' }}>
92
+ Edit your address
93
+ </MyazaText>
94
+ <MyazaText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: 2 }}>
95
+ {anyRequired
96
+ ? 'Correct anything the map got wrong. Fields marked * are required.'
97
+ : 'Correct anything the map got wrong. Every field is optional, and it all helps someone find the door.'}
98
+ </MyazaText>
99
+ </View>
100
+
101
+ <ScrollView
102
+ bounces={false}
103
+ keyboardShouldPersistTaps="handled"
104
+ contentContainerStyle={{ paddingHorizontal: spacing.md, paddingBottom: spacing.sm }}
105
+ >
106
+ {showStreetSection ? <SectionHeading>Street and building</SectionHeading> : null}
107
+ {on('propertyNumber') || on('street') ? (
108
+ <View style={twoCol}>
109
+ {on('propertyNumber') ? (
110
+ <View style={col}>
111
+ <EditField label="Number" value={values.propertyNumber ?? ''} placeholder="e.g. 11" maxLength={20} disabled={disabled} required={req('propertyNumber')} onChange={(propertyNumber) => onChange({ propertyNumber })} />
112
+ </View>
113
+ ) : null}
114
+ {on('street') ? (
115
+ <View style={col}>
116
+ <EditField label="Street name" value={shown(values.street, parts?.street)} placeholder="e.g. Awolowo Road" maxLength={120} disabled={disabled} required={req('street')} onChange={(street) => onChange({ street })} />
117
+ </View>
118
+ ) : null}
119
+ </View>
120
+ ) : null}
121
+ {on('unit') || on('propertyName') ? (
122
+ <View style={[twoCol, { marginTop: spacing.sm }]}>
123
+ {on('unit') ? (
124
+ <View style={col}>
125
+ <EditField label="Unit" value={values.unit ?? ''} placeholder="e.g. Flat 4" maxLength={30} disabled={disabled} required={req('unit')} onChange={(unit) => onChange({ unit })} />
126
+ </View>
127
+ ) : null}
128
+ {on('propertyName') ? (
129
+ <View style={col}>
130
+ <EditField label="Building name" value={values.propertyName ?? ''} placeholder="e.g. Sunrise Villa" maxLength={80} disabled={disabled} required={req('propertyName')} onChange={(propertyName) => onChange({ propertyName })} />
131
+ </View>
132
+ ) : null}
133
+ </View>
134
+ ) : null}
135
+ <View style={{ marginTop: spacing.sm }}>
136
+ <EditField
137
+ label={`${isBusiness ? 'Directions to the entrance' : 'Directions to this address'}${directionsRequired ? '' : ' (optional)'}`}
138
+ value={values.directions ?? ''}
139
+ placeholder="e.g. black gate opposite the kiosk, second building after the junction"
140
+ maxLength={500}
141
+ multiline
142
+ disabled={disabled}
143
+ onChange={(directions) => onChange({ directions })}
144
+ />
145
+ </View>
146
+
147
+ <AreaFields modes={modes} values={values} parts={parts} shown={shown} country={country} disabled={disabled} onChange={onChange} />
148
+ </ScrollView>
149
+
150
+ {/* Bottom padding to match the web sheet's pb-6: the card floats clear
151
+ of the screen edge by 8, so 16 here lands the button the same
152
+ distance off the home indicator as the web drawer sits off the
153
+ bottom of the window. */}
154
+ <View style={{ paddingHorizontal: spacing.md, paddingTop: spacing.sm, paddingBottom: spacing.md }}>
155
+ <MyazaButton label="Done" onPress={onClose} />
156
+ </View>
157
+ </FloatingSheet>
158
+ );
159
+ }
160
+
161
+ /** Kept beside the sheet so the pin step's summary row and this share one
162
+ * shape for the "N details added" count — every editable claim counts. */
163
+ export function detailCount(values: Partial<DetailValues>): number {
164
+ return [
165
+ values.propertyNumber,
166
+ values.street,
167
+ values.unit,
168
+ values.propertyName,
169
+ values.directions,
170
+ values.neighbourhood,
171
+ values.city,
172
+ values.state,
173
+ values.postcode,
174
+ ].filter((v) => v?.trim()).length;
175
+ }