@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,210 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Animated, Pressable, View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../../config/theme';
5
+ import { useTheme } from '../../components/runtime';
6
+ import { Icon, type IconName } from '../../components/Icon';
7
+ import { MyazaText } from '../../components/Typography';
8
+ import { useReduceMotion } from '../../components/StaggerIn';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // The intro screen's three plain-language disclosures — the OkHi-patterned
12
+ // consent copy a data protection review asks to see — as a single-open
13
+ // accordion.
14
+ //
15
+ // The body stays MOUNTED inside a clipped Animated.View so its natural height
16
+ // can be measured by onLayout and then animated; RN has no auto-height
17
+ // transition, and LayoutAnimation needs an experimental opt-in on Android.
18
+ // This uses the SDK's own Animated idiom and works the same on both platforms.
19
+ //
20
+ // Keep this copy in lockstep with the web and Flutter SDKs.
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const HOW_IT_WORKS = {
24
+ foreground:
25
+ 'After you finish, your device periodically confirms it is at this address over the coming days. Only day-level summaries ever leave your phone, never your movements.',
26
+ background:
27
+ 'After you finish, your phone confirms it is at this address over the coming days, even when the app is closed. Only day-level summaries ever leave your phone, never your movements.',
28
+ } as const;
29
+
30
+ const disclosuresFor = (
31
+ background: boolean,
32
+ ): Array<{ icon: IconName; title: string; body: string }> => [
33
+ {
34
+ icon: 'circle-help',
35
+ title: 'How it works',
36
+ body: background ? HOW_IT_WORKS.background : HOW_IT_WORKS.foreground,
37
+ },
38
+ {
39
+ icon: 'sliders',
40
+ title: 'You stay in control',
41
+ body: 'You can turn location off at any time in your device settings. An unfinished check simply expires. It never counts against you.',
42
+ },
43
+ {
44
+ icon: 'shield',
45
+ title: 'Your data is protected',
46
+ body: "Location summaries are used only to confirm this address and are handled under your country's data protection rules.",
47
+ },
48
+ ];
49
+
50
+ export function IntroDisclosures({
51
+ background = false,
52
+ }: {
53
+ /** The workflow opts into OS geofencing: the copy says the app can be closed. */
54
+ background?: boolean;
55
+ }): React.ReactElement {
56
+ const { colors } = useTheme();
57
+ const [openIdx, setOpenIdx] = useState<number | null>(null);
58
+ const DISCLOSURES = disclosuresFor(background);
59
+ return (
60
+ <View
61
+ style={{
62
+ borderWidth: 1,
63
+ borderColor: colors.border,
64
+ borderRadius: radius.md,
65
+ overflow: 'hidden',
66
+ }}
67
+ >
68
+ {DISCLOSURES.map((d, i) => (
69
+ <DisclosureRow
70
+ key={d.title}
71
+ icon={d.icon}
72
+ title={d.title}
73
+ body={d.body}
74
+ open={openIdx === i}
75
+ divider={i < DISCLOSURES.length - 1}
76
+ onToggle={() => setOpenIdx(openIdx === i ? null : i)}
77
+ />
78
+ ))}
79
+ </View>
80
+ );
81
+ }
82
+
83
+ function DisclosureRow({
84
+ icon,
85
+ title,
86
+ body,
87
+ open,
88
+ divider,
89
+ onToggle,
90
+ }: {
91
+ icon: IconName;
92
+ title: string;
93
+ body: string;
94
+ open: boolean;
95
+ divider: boolean;
96
+ onToggle: () => void;
97
+ }): React.ReactElement {
98
+ const { colors } = useTheme();
99
+ const reduceMotion = useReduceMotion();
100
+ const [bodyHeight, setBodyHeight] = useState(0);
101
+ const progress = useRef(new Animated.Value(0)).current;
102
+
103
+ useEffect(() => {
104
+ if (reduceMotion) {
105
+ progress.setValue(open ? 1 : 0);
106
+ return;
107
+ }
108
+ Animated.timing(progress, {
109
+ toValue: open ? 1 : 0,
110
+ duration: 240,
111
+ // Height cannot be driven natively, and the chevron rides the same value
112
+ // so the two stay in step.
113
+ useNativeDriver: false,
114
+ }).start();
115
+ }, [open, progress, reduceMotion]);
116
+
117
+ return (
118
+ <View style={divider ? { borderBottomWidth: 1, borderBottomColor: colors.border } : undefined}>
119
+ <Pressable
120
+ onPress={onToggle}
121
+ accessibilityRole="button"
122
+ accessibilityState={{ expanded: open }}
123
+ accessibilityLabel={title}
124
+ style={({ pressed }) => ({
125
+ flexDirection: 'row',
126
+ alignItems: 'center',
127
+ gap: spacing.sm,
128
+ paddingHorizontal: spacing.md,
129
+ paddingVertical: spacing.md - 2,
130
+ backgroundColor: pressed ? colors.backgroundSecondary : 'transparent',
131
+ })}
132
+ >
133
+ <View
134
+ style={{
135
+ width: 28,
136
+ height: 28,
137
+ borderRadius: radius.xs,
138
+ alignItems: 'center',
139
+ justifyContent: 'center',
140
+ backgroundColor: open ? colors.primary100 : colors.backgroundSecondary,
141
+ }}
142
+ >
143
+ <Icon name={icon} size={15} color={open ? colors.primary : colors.textSecondary} />
144
+ </View>
145
+ <MyazaText variant="bodyMedium" style={{ flex: 1, fontWeight: '500' }}>
146
+ {title}
147
+ </MyazaText>
148
+ <Animated.View
149
+ style={{
150
+ transform: [
151
+ {
152
+ rotate: progress.interpolate({
153
+ inputRange: [0, 1],
154
+ outputRange: ['0deg', '180deg'],
155
+ }),
156
+ },
157
+ ],
158
+ }}
159
+ >
160
+ <Icon name="chevron-down" size={16} color={colors.textSecondary} />
161
+ </Animated.View>
162
+ </Pressable>
163
+
164
+ <Animated.View
165
+ style={{
166
+ overflow: 'hidden',
167
+ opacity: progress,
168
+ // 0 until the body has been measured, so nothing flashes on the
169
+ // first frame.
170
+ height: progress.interpolate({ inputRange: [0, 1], outputRange: [0, bodyHeight] }),
171
+ }}
172
+ >
173
+ <View
174
+ // Text, never a target — and out of flow it would otherwise sit over
175
+ // the rows below on any platform that declines to clip it.
176
+ pointerEvents="none"
177
+ // POSITIONED ABSOLUTELY, and that is the whole trick. A plain child
178
+ // of this clipped, zero-height box reports its natural height, which
179
+ // is why the pattern looks safe — but TEXT does not: it is measured
180
+ // inside a bounded box, so the available height of zero clamps it to
181
+ // zero. The row then animated open to the padding alone and every
182
+ // disclosure expanded to an empty strip. Out of flow the text is
183
+ // measured with no height constraint at all, so what comes back is
184
+ // the real thing.
185
+ onLayout={(e) => {
186
+ const measured = Math.round(e.nativeEvent.layout.height);
187
+ if (measured > 0 && measured !== bodyHeight) setBodyHeight(measured);
188
+ }}
189
+ style={{
190
+ position: 'absolute',
191
+ left: 0,
192
+ right: 0,
193
+ top: 0,
194
+ paddingLeft: spacing.md + 28 + spacing.sm,
195
+ paddingRight: spacing.md,
196
+ paddingBottom: spacing.md,
197
+ }}
198
+ >
199
+ <MyazaText
200
+ variant="bodyMedium"
201
+ color={colors.textSecondary}
202
+ style={{ lineHeight: 21 }}
203
+ >
204
+ {body}
205
+ </MyazaText>
206
+ </View>
207
+ </Animated.View>
208
+ </View>
209
+ );
210
+ }
@@ -0,0 +1,113 @@
1
+ import React from 'react';
2
+ import { 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
+ * The keep-or-update decision after a pin move.
11
+ *
12
+ * A picked address is never silently replaced by a reverse geocode, and never
13
+ * silently kept against the applicant's wishes either. They choose. Rendered on
14
+ * the pin step only while the question is open (shouldAskLabelDecision).
15
+ */
16
+ export function LabelDecisionRow({
17
+ label,
18
+ onKeep,
19
+ onAdopt,
20
+ }: {
21
+ /** The picked address line under question. */
22
+ label: string;
23
+ onKeep: () => void;
24
+ onAdopt: () => void;
25
+ }): React.ReactElement {
26
+ const { colors } = useTheme();
27
+ return (
28
+ <View
29
+ // Web's `border-primary/25 bg-primary/[0.05]`, as Flutter draws it.
30
+ style={{
31
+ borderWidth: 1,
32
+ borderColor: `${colors.primary}40`,
33
+ borderRadius: radius.md,
34
+ backgroundColor: `${colors.primary}0D`,
35
+ paddingHorizontal: spacing.md - 2,
36
+ paddingVertical: spacing.md - 4,
37
+ }}
38
+ >
39
+ <View style={{ flexDirection: 'row', alignItems: 'flex-start', gap: spacing.sm + 2 }}>
40
+ <View
41
+ style={{
42
+ width: 32,
43
+ height: 32,
44
+ borderRadius: radius.full,
45
+ alignItems: 'center',
46
+ justifyContent: 'center',
47
+ backgroundColor: `${colors.primary}1A`,
48
+ }}
49
+ >
50
+ <Icon name="map-pinned" size={16} color={colors.primary} />
51
+ </View>
52
+ <View style={{ flex: 1, minWidth: 0 }}>
53
+ <MyazaText variant="bodyMedium" style={{ fontWeight: '600' }}>
54
+ You moved the pin
55
+ </MyazaText>
56
+ <MyazaText variant="bodySmall" color={colors.textSecondary}>
57
+ Keep{' '}
58
+ <MyazaText variant="bodySmall" style={{ fontWeight: '600' }}>
59
+ {label}
60
+ </MyazaText>{' '}
61
+ as your address, or update it to match the new spot?
62
+ </MyazaText>
63
+ </View>
64
+ </View>
65
+
66
+ {/* Indented under the text (web pl-11). */}
67
+ <View style={{ flexDirection: 'row', gap: spacing.sm, marginTop: spacing.sm + 4, paddingLeft: 44 }}>
68
+ <DecisionButton label="Keep this address" primary onPress={onKeep} />
69
+ <DecisionButton label="Use the pin’s address" onPress={onAdopt} />
70
+ </View>
71
+ </View>
72
+ );
73
+ }
74
+
75
+ function DecisionButton({
76
+ label,
77
+ primary,
78
+ onPress,
79
+ }: {
80
+ label: string;
81
+ primary?: boolean;
82
+ onPress: () => void;
83
+ }): React.ReactElement {
84
+ const { colors } = useTheme();
85
+ return (
86
+ <Pressable
87
+ onPress={onPress}
88
+ accessibilityRole="button"
89
+ accessibilityLabel={label}
90
+ style={({ pressed }) => ({
91
+ flex: 1,
92
+ height: 36,
93
+ alignItems: 'center',
94
+ justifyContent: 'center',
95
+ paddingHorizontal: spacing.sm,
96
+ borderRadius: radius.xs,
97
+ borderWidth: primary ? 0 : 1,
98
+ borderColor: colors.primary,
99
+ backgroundColor: primary ? colors.primary : colors.background,
100
+ opacity: pressed ? 0.8 : 1,
101
+ })}
102
+ >
103
+ <MyazaText
104
+ variant="bodySmall"
105
+ color={primary ? colors.onPrimary : colors.primary}
106
+ style={{ fontWeight: '500' }}
107
+ numberOfLines={1}
108
+ >
109
+ {label}
110
+ </MyazaText>
111
+ </Pressable>
112
+ );
113
+ }
@@ -0,0 +1,105 @@
1
+ import React from 'react';
2
+ import { 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
+ import { LineReveal, LineSkeleton } from '../../components/LineSkeleton';
9
+ import { ADDRESS_LINE_PENDING, ADDRESS_LINE_UNAVAILABLE } from '../../lib/address-line';
10
+ import { detailCount } from './DetailsSheet';
11
+
12
+ /**
13
+ * What the pin currently says, and the way into the details sheet.
14
+ *
15
+ * The line is the composed one (displayAddressLine), the same string the review
16
+ * step heads its card with, so the applicant confirms exactly what they were
17
+ * shown here.
18
+ */
19
+ export function PinSummaryRow({
20
+ line,
21
+ labelling,
22
+ values,
23
+ disabled,
24
+ onPress,
25
+ }: {
26
+ /** The composed line; '' while the pin has no readable address yet, and
27
+ * null when no pin has been placed at all. */
28
+ line: string | null;
29
+ /** A reverse geocode is out, so '' means "coming" rather than "none". */
30
+ labelling: boolean;
31
+ values: Parameters<typeof detailCount>[0];
32
+ disabled: boolean;
33
+ onPress: () => void;
34
+ }): React.ReactElement {
35
+ const { colors } = useTheme();
36
+ const count = detailCount(values);
37
+ // Three states, and none of them is a coordinate pair: no pin at all, a pin
38
+ // whose address is still being read (a skeleton line at the text's own
39
+ // height, so nothing moves when the words land), and a pin the geocoder had
40
+ // no address for.
41
+ const pending = line === '' && labelling;
42
+ const title = line === null ? 'No pin placed yet' : line || ADDRESS_LINE_UNAVAILABLE;
43
+
44
+ return (
45
+ <View
46
+ style={{
47
+ flexDirection: 'row',
48
+ alignItems: 'center',
49
+ gap: spacing.sm,
50
+ paddingHorizontal: spacing.md - 2,
51
+ paddingVertical: spacing.sm + 4,
52
+ borderWidth: 1,
53
+ borderColor: colors.border,
54
+ borderRadius: radius.md,
55
+ backgroundColor: colors.backgroundSecondary,
56
+ }}
57
+ >
58
+ <View style={{ flex: 1, minWidth: 0 }}>
59
+ {pending ? (
60
+ <LineSkeleton label={ADDRESS_LINE_PENDING} variant="bodyMedium" textStyle={{ fontWeight: '600' }} />
61
+ ) : (
62
+ <LineReveal key={title}>
63
+ <MyazaText
64
+ variant="bodyMedium"
65
+ style={{ fontWeight: '600' }}
66
+ color={line ? undefined : colors.textSecondary}
67
+ numberOfLines={1}
68
+ >
69
+ {title}
70
+ </MyazaText>
71
+ </LineReveal>
72
+ )}
73
+ <MyazaText variant="bodySmall" color={colors.textSecondary} numberOfLines={1}>
74
+ {count > 0
75
+ ? `${count} detail${count === 1 ? '' : 's'} added`
76
+ : 'A house number and directions help someone find it'}
77
+ </MyazaText>
78
+ </View>
79
+
80
+ <Pressable
81
+ onPress={disabled ? undefined : onPress}
82
+ disabled={disabled}
83
+ accessibilityRole="button"
84
+ accessibilityLabel="Edit details"
85
+ style={({ pressed }) => ({
86
+ flexDirection: 'row',
87
+ alignItems: 'center',
88
+ gap: spacing.xs + 2,
89
+ height: 36,
90
+ paddingHorizontal: spacing.sm + 4,
91
+ borderRadius: radius.xs,
92
+ borderWidth: 1,
93
+ borderColor: colors.border,
94
+ backgroundColor: pressed ? colors.primary50 : colors.background,
95
+ opacity: disabled ? 0.5 : 1,
96
+ })}
97
+ >
98
+ <Icon name="pencil-line" size={14} color={colors.primary} />
99
+ <MyazaText variant="bodyMedium" color={colors.primary} style={{ fontWeight: '600' }}>
100
+ Edit details
101
+ </MyazaText>
102
+ </Pressable>
103
+ </View>
104
+ );
105
+ }
@@ -0,0 +1,111 @@
1
+ import React from 'react';
2
+ import { Pressable, View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../../config/theme';
5
+ import { useTheme } from '../../components/runtime';
6
+ import { MyazaText } from '../../components/Typography';
7
+ import { Icon } from '../../components/Icon';
8
+ import { LineSkeleton } from '../../components/LineSkeleton';
9
+ import { ADDRESS_LINE_PENDING, ADDRESS_LINE_UNAVAILABLE } from '../../lib/address-line';
10
+
11
+ // The address band under the review card's map (200-line split from
12
+ // AddressReviewStep): the success card's header-band language, so the
13
+ // confirmation and the "check active" card read as one design. Web's exact
14
+ // geometry: the band's row clears the thumbnails with pr-32 (128) and
15
+ // min-h-[4.25rem] (68). Flutter draws the same band. EXACTLY 128: the 12 of
16
+ // breathing room once added here starved the "Pinned address" pill on a
17
+ // 360dp phone (the S24, 2026-09-08), so the label wrapped inside its pill
18
+ // here and overflowed on Flutter.
19
+
20
+ const BAND_CLEARANCE = 128;
21
+ const BAND_MIN_HEIGHT = 68;
22
+
23
+ export function ReviewAddressBand({
24
+ isBusiness,
25
+ hasAddress,
26
+ line,
27
+ labelling,
28
+ directions,
29
+ clearsHero,
30
+ onEdit,
31
+ }: {
32
+ isBusiness: boolean;
33
+ hasAddress: boolean;
34
+ /** The composed line; '' while the pin has no readable address yet. */
35
+ line: string;
36
+ labelling: boolean;
37
+ directions: string | null;
38
+ /** Something hangs over the map's edge: the band clears it. */
39
+ clearsHero: boolean;
40
+ onEdit: () => void;
41
+ }): React.ReactElement {
42
+ const { colors } = useTheme();
43
+ return (
44
+ <View
45
+ style={{
46
+ backgroundColor: `${colors.primary}0F`,
47
+ paddingHorizontal: spacing.md,
48
+ paddingVertical: spacing.md,
49
+ paddingRight: clearsHero ? BAND_CLEARANCE : spacing.md,
50
+ minHeight: clearsHero ? BAND_MIN_HEIGHT : undefined,
51
+ borderBottomLeftRadius: radius.md - 1,
52
+ borderBottomRightRadius: radius.md - 1,
53
+ }}
54
+ >
55
+ <View style={{ flexDirection: 'row', alignItems: 'flex-start', gap: spacing.sm }}>
56
+ <View style={{ flex: 1, minWidth: 0 }}>
57
+ <View
58
+ style={{
59
+ flexDirection: 'row',
60
+ alignItems: 'center',
61
+ alignSelf: 'flex-start',
62
+ gap: spacing.sm,
63
+ maxWidth: '100%',
64
+ paddingHorizontal: spacing.sm + 2,
65
+ paddingVertical: 4,
66
+ borderRadius: radius.full,
67
+ backgroundColor: `${colors.primary}1A`,
68
+ }}
69
+ >
70
+ <Icon name="map-pin" size={12} color={colors.primary} />
71
+ {/* One line, shrinking with an ellipsis when starved (a large
72
+ text scale), as Flutter's pill does. */}
73
+ <MyazaText
74
+ variant="bodySmall"
75
+ color={colors.primary}
76
+ numberOfLines={1}
77
+ style={{ flexShrink: 1, fontSize: 11, fontWeight: '600', letterSpacing: 0.6, textTransform: 'uppercase' }}
78
+ >
79
+ {isBusiness ? 'Pinned premises' : 'Pinned address'}
80
+ </MyazaText>
81
+ </View>
82
+
83
+ {/* Never coordinates: an unread pin shows a skeleton line while
84
+ its address is still coming. Web's `text-base font-semibold`:
85
+ the body face, not a heading. */}
86
+ {hasAddress && !line && labelling ? (
87
+ <LineSkeleton label={ADDRESS_LINE_PENDING} variant="body" barHeight={12} width="70%" style={{ marginTop: spacing.xs }} />
88
+ ) : (
89
+ <MyazaText variant="body" style={{ marginTop: spacing.xs, fontWeight: '600' }}>
90
+ {!hasAddress ? 'No pin placed' : line || ADDRESS_LINE_UNAVAILABLE}
91
+ </MyazaText>
92
+ )}
93
+
94
+ {directions ? (
95
+ <MyazaText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: 2 }}>
96
+ {`“${directions}”`}
97
+ </MyazaText>
98
+ ) : null}
99
+ </View>
100
+
101
+ {/* A plain link (web `text-sm text-primary`); the band's right
102
+ padding keeps it clear of the entrance hanging over it. */}
103
+ <Pressable onPress={onEdit} accessibilityRole="button" accessibilityLabel="Edit" hitSlop={8}>
104
+ <MyazaText variant="bodyMedium" color={colors.primary}>
105
+ Edit
106
+ </MyazaText>
107
+ </Pressable>
108
+ </View>
109
+ </View>
110
+ );
111
+ }
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import { Image, View, type ImageSourcePropType } from 'react-native';
3
+
4
+ import { radius } from '../../config/theme';
5
+ import { useTheme } from '../../components/runtime';
6
+
7
+ // The entrance thumbnails on the review card (200-line split from
8
+ // AddressReviewStep). Web's exact geometry, so the three cards are one card:
9
+ // hero h-28 (112) rounded-2xl shadow-xl at -bottom-10 right-4
10
+ // second h-20 (80) rounded-xl shadow-lg at right-36 (144)
11
+ // Both share one bottom edge, 40 below the map. Flutter draws the same.
12
+
13
+ export const HERO = 112;
14
+ export const SECOND = 80;
15
+ export const OVERHANG = 40;
16
+
17
+ export function ReviewEntranceThumb({
18
+ source,
19
+ label,
20
+ size,
21
+ right,
22
+ onFailed,
23
+ }: {
24
+ source: ImageSourcePropType;
25
+ label: string;
26
+ size: number;
27
+ right: number;
28
+ onFailed: (error: unknown) => void;
29
+ }): React.ReactElement {
30
+ const { colors } = useTheme();
31
+ // The hero wears the card's radius (16), the smaller one a step down (12).
32
+ const r = size === HERO ? radius.md : radius.sm;
33
+ return (
34
+ // The shadow lives on a wrapper with a background of its own: an Image
35
+ // cannot cast one on Android, and iOS needs an opaque shape to draw it
36
+ // from. zIndex 20, as web spells it, so the half hanging over the band is
37
+ // not covered by it.
38
+ <View
39
+ style={{
40
+ position: 'absolute',
41
+ zIndex: 20,
42
+ right,
43
+ bottom: -OVERHANG,
44
+ width: size,
45
+ height: size,
46
+ borderRadius: r,
47
+ backgroundColor: colors.background,
48
+ shadowColor: colors.textDark,
49
+ shadowOpacity: 0.18,
50
+ shadowRadius: 12,
51
+ shadowOffset: { width: 0, height: 4 },
52
+ elevation: 6,
53
+ }}
54
+ >
55
+ <Image
56
+ source={source}
57
+ accessibilityLabel={label}
58
+ onError={(e) => onFailed(e.nativeEvent?.error)}
59
+ style={{ width: size, height: size, borderRadius: r, borderWidth: 4, borderColor: colors.background }}
60
+ resizeMode="cover"
61
+ />
62
+ </View>
63
+ );
64
+ }