@myazahq/kyc-sdk-react-native 2.5.0 → 3.0.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 (219) hide show
  1. package/README.md +277 -7
  2. package/android/build.gradle +30 -16
  3. package/android/consumer-rules.pro +22 -0
  4. package/android/src/main/AndroidManifest.xml +17 -6
  5. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +27 -70
  6. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +14 -0
  7. package/android/src/main/java/com/margelo/nitro/myazakyc/MlKitModelReadiness.kt +154 -0
  8. package/app.plugin.js +146 -10
  9. package/ios/HybridMyazaFaceDetector.swift +8 -3
  10. package/ios/HybridMyazaTextRecognizer.swift +8 -0
  11. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +21 -0
  12. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +2 -0
  13. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +8 -0
  14. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +16 -0
  15. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +2 -0
  16. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +31 -0
  17. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +2 -0
  18. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +2 -0
  19. package/package.json +30 -9
  20. package/src/MyazaKYC.tsx +4 -11
  21. package/src/assets/fonts/Karla_400Regular.ttf +0 -0
  22. package/src/assets/fonts/Karla_500Medium.ttf +0 -0
  23. package/src/assets/fonts/Karla_600SemiBold.ttf +0 -0
  24. package/src/assets/fonts/Karla_700Bold.ttf +0 -0
  25. package/src/assets/fonts/OFL-Karla.txt +93 -0
  26. package/src/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
  27. package/src/assets/fonts/README.md +12 -0
  28. package/src/assets/fonts/SpaceGrotesk_500Medium.ttf +0 -0
  29. package/src/assets/fonts/SpaceGrotesk_600SemiBold.ttf +0 -0
  30. package/src/assets/fonts/SpaceGrotesk_700Bold.ttf +0 -0
  31. package/src/capture/useAutoCapture.ts +8 -1
  32. package/src/components/CountryField.tsx +8 -0
  33. package/src/components/CountryRegionPicker.tsx +184 -0
  34. package/src/components/DialCodePicker.tsx +47 -68
  35. package/src/components/DialCodeRow.tsx +102 -0
  36. package/src/components/DocumentCropper.tsx +30 -24
  37. package/src/components/DocumentReview.tsx +11 -1
  38. package/src/components/DocumentReviewSide.tsx +6 -2
  39. package/src/components/DocumentReviewZoom.tsx +5 -1
  40. package/src/components/FramedMapPicker.tsx +122 -0
  41. package/src/components/GeoBadge.tsx +34 -0
  42. package/src/components/Icon.tsx +10 -192
  43. package/src/components/KycFlow.tsx +69 -34
  44. package/src/components/LineSkeleton.tsx +100 -0
  45. package/src/components/MapChrome.tsx +67 -0
  46. package/src/components/MapPinMarker.tsx +54 -0
  47. package/src/components/MapPinPicker.tsx +179 -0
  48. package/src/components/MilestoneTrack.tsx +155 -0
  49. package/src/components/MyazaButton.tsx +9 -0
  50. package/src/components/MyazaInput.tsx +17 -1
  51. package/src/components/OptionRow.tsx +22 -3
  52. package/src/components/PhoneNumberInput.tsx +8 -0
  53. package/src/components/PoweredBy.tsx +5 -5
  54. package/src/components/PresenceBlocks.tsx +153 -0
  55. package/src/components/RequiredDocumentPill.tsx +114 -0
  56. package/src/components/StepHeader.tsx +23 -3
  57. package/src/components/StepView.tsx +17 -0
  58. package/src/components/StickyActions.tsx +56 -0
  59. package/src/components/dialCodeRows.ts +64 -0
  60. package/src/components/documentReviewCopy.ts +39 -0
  61. package/src/components/fonts.ts +14 -17
  62. package/src/components/icon-map.ts +176 -0
  63. package/src/components/stepHeaderMeta.tsx +48 -5
  64. package/src/config/addressCollection.ts +125 -0
  65. package/src/config/biometricOptions.ts +100 -0
  66. package/src/config/business.ts +20 -1
  67. package/src/config/businessSteps.ts +8 -2
  68. package/src/config/consentStep.ts +19 -0
  69. package/src/config/documentCaptureMethods.ts +31 -0
  70. package/src/config/proofOfAddress.ts +54 -11
  71. package/src/config/regions.ts +24 -0
  72. package/src/config/stepOrder.ts +79 -5
  73. package/src/config/uploadLimits.ts +35 -0
  74. package/src/config/workflowMerge.ts +13 -5
  75. package/src/index.ts +49 -1
  76. package/src/lib/address-current-location.ts +171 -0
  77. package/src/lib/address-field-modes.ts +119 -0
  78. package/src/lib/address-flow.ts +172 -0
  79. package/src/lib/address-helpers.ts +48 -0
  80. package/src/lib/address-line.ts +76 -0
  81. package/src/lib/address-pin-move.ts +97 -0
  82. package/src/lib/address-step-recovery.ts +63 -0
  83. package/src/lib/authed-image.ts +78 -0
  84. package/src/lib/biometric-copy.ts +51 -0
  85. package/src/lib/captureRing.ts +83 -0
  86. package/src/lib/country-adoption.ts +89 -0
  87. package/src/lib/documentCaptureCheck.ts +136 -0
  88. package/src/lib/inferred-country.ts +65 -0
  89. package/src/lib/livenessLayout.ts +49 -0
  90. package/src/lib/map-frame.ts +159 -0
  91. package/src/lib/map-tiles.ts +143 -0
  92. package/src/lib/model-ready.ts +82 -0
  93. package/src/lib/poa-country-gate.ts +27 -0
  94. package/src/lib/prime-models.ts +50 -0
  95. package/src/lib/resubmit.ts +36 -7
  96. package/src/lib/result-copy.ts +116 -0
  97. package/src/lib/result-wait.ts +53 -0
  98. package/src/lib/review-map-surface.ts +26 -0
  99. package/src/lib/scope.ts +31 -0
  100. package/src/lib/selfie-sharpness.ts +211 -0
  101. package/src/lib/selfie-upload-wait.ts +79 -0
  102. package/src/lib/street-view-fov.ts +42 -0
  103. package/src/lib/webview-available.ts +47 -0
  104. package/src/liveness/avatarSource.ts +59 -0
  105. package/src/liveness/useLiveness.ts +6 -1
  106. package/src/liveness/useModelReady.ts +9 -55
  107. package/src/mrz/extract.ts +71 -4
  108. package/src/mrz/textRecognizer.ts +36 -0
  109. package/src/mrz/useTextModelReady.ts +21 -0
  110. package/src/presence/background-math.ts +100 -0
  111. package/src/presence/background-store.ts +82 -0
  112. package/src/presence/background.ts +165 -0
  113. package/src/presence/foreground-service.ts +193 -0
  114. package/src/presence/fs.ts +51 -0
  115. package/src/presence/geofence.ts +41 -0
  116. package/src/presence/math.ts +44 -0
  117. package/src/presence/post.ts +40 -0
  118. package/src/presence/report.ts +87 -0
  119. package/src/presence/sampler.ts +110 -0
  120. package/src/presence/status.ts +92 -0
  121. package/src/presence/store.ts +94 -0
  122. package/src/presence/tier.ts +36 -0
  123. package/src/presence/watch-wait.ts +81 -0
  124. package/src/screens/AddressCountryControl.tsx +115 -0
  125. package/src/screens/BusinessDetailsFields.tsx +3 -0
  126. package/src/screens/BusinessDetailsStep.tsx +2 -0
  127. package/src/screens/BusinessDocumentSlot.tsx +3 -2
  128. package/src/screens/BusinessDocumentsStep.tsx +4 -55
  129. package/src/screens/CompanyInfoFields.tsx +6 -1
  130. package/src/screens/ConsentStep.tsx +6 -17
  131. package/src/screens/ContactActions.tsx +54 -0
  132. package/src/screens/ContactDestinationField.tsx +4 -0
  133. package/src/screens/ContactEntryPanel.tsx +75 -0
  134. package/src/screens/ContactVerificationStep.tsx +26 -56
  135. package/src/screens/CountrySelectStep.tsx +13 -147
  136. package/src/screens/DocumentCaptureStep.tsx +177 -93
  137. package/src/screens/LivenessAvatar.tsx +33 -16
  138. package/src/screens/LivenessStep.tsx +188 -49
  139. package/src/screens/MrzScanView.tsx +33 -0
  140. package/src/screens/PoaDocumentTypeList.tsx +66 -0
  141. package/src/screens/ProofOfAddressParts.tsx +151 -0
  142. package/src/screens/ProofOfAddressStep.tsx +58 -99
  143. package/src/screens/SubmittedBadge.tsx +25 -0
  144. package/src/screens/SubmittedError.tsx +64 -0
  145. package/src/screens/SubmittedResult.tsx +117 -0
  146. package/src/screens/SubmittedStep.tsx +71 -164
  147. package/src/screens/SubmittedSuccess.tsx +127 -0
  148. package/src/screens/SubmittedWaiting.tsx +45 -0
  149. package/src/screens/address/AddressEntranceStep.tsx +161 -0
  150. package/src/screens/address/AddressIntroGate.tsx +148 -0
  151. package/src/screens/address/AddressMapStub.tsx +50 -0
  152. package/src/screens/address/AddressPinStep.tsx +178 -0
  153. package/src/screens/address/AddressReviewStep.tsx +162 -0
  154. package/src/screens/address/AddressSandboxTabs.tsx +158 -0
  155. package/src/screens/address/AddressSearchField.tsx +115 -0
  156. package/src/screens/address/AddressSearchStep.tsx +94 -0
  157. package/src/screens/address/CurrentLocationRow.tsx +135 -0
  158. package/src/screens/address/DetailsSheet.tsx +175 -0
  159. package/src/screens/address/DetailsSheetFields.tsx +184 -0
  160. package/src/screens/address/EntranceDropzone.tsx +178 -0
  161. package/src/screens/address/EntranceFraming.tsx +66 -0
  162. package/src/screens/address/EntrancePills.tsx +60 -0
  163. package/src/screens/address/FramedStreetView.tsx +155 -0
  164. package/src/screens/address/IntroDisclosures.tsx +210 -0
  165. package/src/screens/address/LabelDecisionRow.tsx +113 -0
  166. package/src/screens/address/PinSummaryRow.tsx +105 -0
  167. package/src/screens/address/ReviewAddressBand.tsx +111 -0
  168. package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
  169. package/src/screens/address/ReviewMapPicture.tsx +106 -0
  170. package/src/screens/address/SearchResults.tsx +105 -0
  171. package/src/screens/address/SearchScreen.tsx +176 -0
  172. package/src/screens/address/SkipForNow.tsx +43 -0
  173. package/src/screens/address/StreetViewChrome.tsx +81 -0
  174. package/src/screens/address/detail-values.ts +22 -0
  175. package/src/screens/address/fix-source.ts +27 -0
  176. package/src/screens/address/index.ts +8 -0
  177. package/src/screens/address/meta.ts +44 -0
  178. package/src/screens/address/use-address-flow.ts +200 -0
  179. package/src/screens/address/use-label-pin.ts +80 -0
  180. package/src/screens/address/use-pin-actions.ts +192 -0
  181. package/src/screens/consent/model.ts +79 -10
  182. package/src/screens/document/CaptureCheckNotice.tsx +77 -0
  183. package/src/screens/document/UploadPhase.tsx +177 -0
  184. package/src/screens/document/useDocumentCamera.ts +109 -0
  185. package/src/screens/liveness/CaptureRing.tsx +91 -0
  186. package/src/screens/liveness/LivenessCamera.tsx +63 -0
  187. package/src/screens/liveness/LivenessHandover.tsx +43 -0
  188. package/src/screens/liveness/LivenessOutcome.tsx +8 -4
  189. package/src/screens/liveness/SelfiePreview.tsx +31 -7
  190. package/src/screens/liveness/ShutterFlash.tsx +25 -0
  191. package/src/screens/liveness/index.ts +4 -0
  192. package/src/screens/liveness/useSelfieUpload.ts +23 -3
  193. package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
  194. package/src/screens/useAddressPhotoAttach.ts +77 -0
  195. package/src/screens/useBusinessDocumentAttach.ts +93 -0
  196. package/src/screens/usePoaAttach.ts +12 -15
  197. package/src/services/api-types.ts +119 -1
  198. package/src/services/api-verify-types.ts +37 -0
  199. package/src/services/api.ts +149 -3
  200. package/src/services/deviceMetadata.ts +1 -1
  201. package/src/services/location.ts +234 -0
  202. package/src/services/mediaCompress.ts +23 -5
  203. package/src/services/uploadErrors.ts +30 -0
  204. package/src/services/workflowGate.ts +7 -0
  205. package/src/specs/MyazaTextRecognizer.nitro.ts +29 -0
  206. package/src/store/address.ts +97 -0
  207. package/src/store/derive.ts +29 -4
  208. package/src/store/kycStore.ts +120 -5
  209. package/src/store/serverConfig.ts +18 -0
  210. package/src/store/session.ts +23 -4
  211. package/src/store/state.ts +134 -0
  212. package/src/store/submit.ts +18 -1
  213. package/src/types/config.ts +63 -1
  214. package/src/types/verification.ts +12 -0
  215. package/src/types/workflow.ts +97 -1
  216. package/src/assets/liveness/Blink.gif +0 -0
  217. package/src/assets/liveness/Nod.gif +0 -0
  218. package/src/assets/liveness/Smile.gif +0 -0
  219. package/src/assets/liveness/Turn.gif +0 -0
@@ -1,192 +1,8 @@
1
1
  import React from 'react';
2
- import Ionicons from '@expo/vector-icons/Ionicons';
3
- import {
4
- Info,
5
- BadgeCheck,
6
- BookUser,
7
- Building2,
8
- Calendar,
9
- Camera,
10
- CameraOff,
11
- ChevronDown,
12
- ChevronLeft,
13
- ChevronRight,
14
- Car,
15
- Check,
16
- CircleAlert,
17
- Contact,
18
- CreditCard,
19
- Fingerprint,
20
- FileText,
21
- FlaskConical,
22
- IdCard,
23
- Image as ImageIcon,
24
- Landmark,
25
- Lightbulb,
26
- Globe,
27
- Mail,
28
- Maximize2,
29
- Lock,
30
- MessageCircle,
31
- MessageSquare,
32
- Moon,
33
- MoveLeft,
34
- Nfc,
35
- Filter,
36
- Pencil,
37
- Smartphone,
38
- RefreshCw,
39
- ScanFace,
40
- ScanLine,
41
- Search,
42
- Zap,
43
- ZapOff,
44
- ShieldCheck,
45
- Sun,
46
- Timer,
47
- Upload,
48
- Plus,
49
- UserRound,
50
- UserRoundPlus,
51
- Link2,
52
- Copy,
53
- Share2,
54
- UsersRound,
55
- VideoOff,
56
- Vote,
57
- X,
58
- type LucideIcon,
59
- } from 'lucide-react-native';
60
2
 
61
- // Centralised icon set — the SAME Lucide icons the Flutter SDK uses
62
- // (`lucide_icons_flutter`) and the web SDK uses (`lucide-react`). Rendered via
63
- // lucide-react-native (react-native-svg), so the glyphs are pixel-identical
64
- // across all three platforms.
3
+ import { ICONS, type IconName } from './icon-map';
65
4
 
66
- export type IconName =
67
- // chrome
68
- | 'close'
69
- | 'back'
70
- | 'moon'
71
- | 'sun'
72
- | 'check'
73
- | 'lock'
74
- | 'alert'
75
- | 'flask'
76
- | 'refresh'
77
- | 'maximize'
78
- | 'calendar'
79
- | 'image'
80
- | 'chevron-left'
81
- | 'chevron-right'
82
- | 'chevron-down'
83
- // consent hero + process steps
84
- | 'shield'
85
- | 'badge-check'
86
- | 'building-2'
87
- | 'user'
88
- | 'user-plus'
89
- | 'plus'
90
- | 'info'
91
- | 'users'
92
- | 'scan-line'
93
- | 'scan-face'
94
- | 'nfc'
95
- | 'filter'
96
- | 'pencil'
97
- | 'search'
98
- | 'zap'
99
- | 'link'
100
- | 'copy'
101
- | 'share'
102
- | 'zap-off'
103
- | 'timer'
104
- // id-type glyphs
105
- | 'fingerprint'
106
- | 'credit-card'
107
- | 'globe'
108
- | 'car'
109
- | 'vote'
110
- | 'file-text'
111
- | 'landmark'
112
- | 'id-card'
113
- | 'contact'
114
- | 'passport'
115
- | 'camera'
116
- | 'camera-off'
117
- | 'video-off'
118
- | 'upload'
119
- | 'x'
120
- | 'lightbulb'
121
- // contact verification (OTP channels + step footer)
122
- | 'mail'
123
- | 'smartphone'
124
- | 'message-square'
125
- | 'message-circle';
126
-
127
- // Maps each name to its Lucide component. Names mirror the Flutter SDK exactly:
128
- // shieldCheck · badgeCheck · userRound · scanLine · scanFace · fingerprint ·
129
- // creditCard · globe · car · vote · fileText (+ chrome: X / ArrowLeft /
130
- // Moon / Sun / Check / Lock / CircleAlert / RefreshCw / Camera).
131
- const ICONS: Record<IconName, LucideIcon> = {
132
- close: X,
133
- back: MoveLeft, // longer-shaft left arrow, matching Flutter's keyboard_backspace
134
- moon: Moon,
135
- sun: Sun,
136
- check: Check,
137
- lock: Lock,
138
- alert: CircleAlert,
139
- // The environment banner's mark, shared with the web SDK (same lucide glyph)
140
- // and mirrored by Flutter's Icons.science_outlined.
141
- flask: FlaskConical,
142
- refresh: RefreshCw,
143
- maximize: Maximize2,
144
- calendar: Calendar,
145
- image: ImageIcon,
146
- 'chevron-left': ChevronLeft,
147
- 'chevron-right': ChevronRight,
148
- 'chevron-down': ChevronDown,
149
- shield: ShieldCheck,
150
- 'badge-check': BadgeCheck,
151
- 'building-2': Building2,
152
- user: UserRound,
153
- info: Info,
154
- 'user-plus': UserRoundPlus,
155
- plus: Plus,
156
- link: Link2,
157
- copy: Copy,
158
- share: Share2,
159
- users: UsersRound,
160
- 'scan-line': ScanLine,
161
- 'scan-face': ScanFace,
162
- nfc: Nfc,
163
- filter: Filter,
164
- pencil: Pencil,
165
- search: Search,
166
- zap: Zap,
167
- 'zap-off': ZapOff,
168
- timer: Timer,
169
- fingerprint: Fingerprint,
170
- 'credit-card': CreditCard,
171
- globe: Globe,
172
- car: Car,
173
- vote: Vote,
174
- 'file-text': FileText,
175
- landmark: Landmark,
176
- 'id-card': IdCard,
177
- contact: Contact,
178
- passport: BookUser,
179
- camera: Camera,
180
- 'camera-off': CameraOff,
181
- 'video-off': VideoOff,
182
- upload: Upload,
183
- x: X,
184
- lightbulb: Lightbulb,
185
- mail: Mail,
186
- smartphone: Smartphone,
187
- 'message-square': MessageSquare,
188
- 'message-circle': MessageCircle,
189
- };
5
+ export type { IconName } from './icon-map';
190
6
 
191
7
  export interface IconProps {
192
8
  name: IconName;
@@ -198,11 +14,13 @@ export interface IconProps {
198
14
  }
199
15
 
200
16
  export function Icon({ name, size = 20, color, strokeWidth = 2, solid = false }: IconProps): React.ReactElement {
201
- // Solid moon/sun for the theme toggle — Lucide has no filled variants, so use
202
- // Ionicons' purpose-built solid glyphs (matches Flutter's dark_mode/light_mode).
203
- if (solid && (name === 'moon' || name === 'sun')) {
204
- return <Ionicons name={name === 'moon' ? 'moon' : 'sunny'} size={size} color={color} />;
205
- }
206
17
  const Glyph = ICONS[name];
207
- return <Glyph size={size} color={color} strokeWidth={strokeWidth} />;
18
+ // The theme toggle's filled moon/sun (Flutter's dark_mode/light_mode). Lucide
19
+ // ships no filled variants, so the glyph's own body takes the colour: the
20
+ // moon's crescent and the sun's disc fill, the sun's rays stay strokes. This
21
+ // replaced an Ionicons import that was the SDK's ONLY use of
22
+ // @expo/vector-icons, a package whose icon fonts still reached every
23
+ // integrator's app for those two glyphs (1.35 MB on a real release APK).
24
+ const fill = solid && (name === 'moon' || name === 'sun') ? color : 'none';
25
+ return <Glyph size={size} color={color} strokeWidth={strokeWidth} fill={fill} />;
208
26
  }
@@ -3,6 +3,7 @@ import { View } from 'react-native';
3
3
 
4
4
  import { radius, spacing } from '../config/theme';
5
5
  import { buildStepOrder } from '../config/stepOrder';
6
+ import { isAddressStep } from '../lib/address-flow';
6
7
  import { stepOrderOptions } from '../store/kycStore';
7
8
  import { KYCError } from '../types/verification';
8
9
  import { safeReportError } from '../services/errors';
@@ -50,6 +51,7 @@ export function KycFlow({
50
51
  const contactChallenge = useKyc((s) => s.contactChallenge);
51
52
  const serverConfig = useKyc((s) => s.serverConfig);
52
53
  const immersiveCapture = useKyc((s) => s.immersiveCapture);
54
+ const addressIntroSeen = useKyc((s) => s.addressIntroSeen);
53
55
 
54
56
  const startedRef = useRef(false);
55
57
  const reportedRef = useRef(false);
@@ -76,7 +78,26 @@ export function KycFlow({
76
78
  }
77
79
  }, [isFatal, serverConfig.statusCode, serverConfig.message, config.onError]);
78
80
 
79
- // ── Per-step header meta ──────────────────────────────────────────────────
81
+ // ── The flow's ordered steps ──────────────────────────────────────────────
82
+ // ONE list, read by the step indicator AND by the address primer below, so
83
+ // neither can describe a flow the user is not walking. This used to be a
84
+ // hand-written copy of the sequence, which meant a step added to the flow
85
+ // silently didn't count towards progress.
86
+ //
87
+ // The derivation is memoised rather than done inside the selector: zustand
88
+ // compares snapshots by identity, so a selector that mints a fresh object on
89
+ // every call re-renders forever.
90
+ const state = useKyc((s) => s);
91
+ const order = useMemo(() => buildStepOrder(stepOrderOptions(state)), [state]);
92
+
93
+ // The presence primer replaces the FIRST address step's body and carries its
94
+ // own title, so the header blanks while it is up. Matched against the order
95
+ // rather than a second copy of "which step comes first".
96
+ const addressIntroPending =
97
+ config.addressCollection?.presence?.enabled === true &&
98
+ !addressIntroSeen &&
99
+ order.find(isAddressStep) === currentStep;
100
+
80
101
  const meta = useMemo(
81
102
  () =>
82
103
  stepHeaderMeta(currentStep, {
@@ -85,40 +106,55 @@ export function KycFlow({
85
106
  selectedIdType,
86
107
  documentCapturePhase,
87
108
  contactChallenge,
109
+ addressIntroPending,
110
+ addressEntranceFraming: state.addressEntranceFraming,
111
+ poaDocumentType: state.poaDocumentType,
88
112
  }),
89
- [currentStep, config, country, selectedIdType, documentCapturePhase, contactChallenge],
113
+ [
114
+ currentStep,
115
+ config,
116
+ country,
117
+ selectedIdType,
118
+ documentCapturePhase,
119
+ contactChallenge,
120
+ addressIntroPending,
121
+ state.addressEntranceFraming,
122
+ state.poaDocumentType,
123
+ ],
90
124
  );
91
125
 
92
126
  // ── Step indicator info ───────────────────────────────────────────────────
93
- // Read from the SAME ordered list navigation uses, so the dots can never
94
- // describe a different flow than the one the user is walking. This used to be
95
- // its own hand-written copy of the sequence, which meant a step added to the
96
- // flow silently didn't count towards progress.
97
- //
98
127
  // 'submitted' is excluded on purpose — it is the terminal screen, not a step
99
128
  // to make progress towards, and the indicator is hidden there.
100
- //
101
- // The derivation is memoised rather than done inside the selector: zustand
102
- // compares snapshots by identity, so a selector that mints a fresh object on
103
- // every call re-renders forever.
104
- const state = useKyc((s) => s);
105
129
  const stepInfo = useMemo(() => {
106
- if (state.currentStep === 'submitted') return null;
107
- const steps = buildStepOrder(stepOrderOptions(state)).filter((s) => s !== 'submitted');
108
- const idx = steps.indexOf(state.currentStep);
130
+ if (currentStep === 'submitted') return null;
131
+ const steps = order.filter((s) => s !== 'submitted');
132
+ const idx = steps.indexOf(currentStep);
109
133
  if (idx < 0) return null;
110
134
  return { progress: (idx + 1) / steps.length, stepCount: steps.length };
111
- }, [state]);
135
+ }, [currentStep, order]);
112
136
 
113
- // The flag beside the title names the country whose IDs are on screen.
137
+ // The flag beside the title names the country whose IDs are on screen. The
138
+ // document steps ask for a country's document just as directly as the ID
139
+ // ones do, so they carry the same flag rather than a second treatment.
114
140
  const headerCountry =
115
- currentStep === 'id-type' || currentStep === 'id-input' ? country : null;
116
- const onBack = currentStep === 'consent' || currentStep === 'submitted' ? null : () => store.getState().previousStep();
141
+ currentStep === 'id-type' ||
142
+ currentStep === 'id-input' ||
143
+ currentStep === 'document-capture' ||
144
+ currentStep === 'nfc'
145
+ ? country
146
+ : null;
147
+ // Back is hidden on the flow's OPENING step (consent, or the first real step
148
+ // when the workflow switched the consent screen off), not on consent by
149
+ // name. A multi-ID run returns to the ID picker for its next check, where
150
+ // Back means "redo the previous one", so a committed slot keeps it.
151
+ const onOpeningStep = currentStep === order[0] && state.multiIdSlots.length === 0;
152
+ const onBack = onOpeningStep || currentStep === 'submitted' ? null : () => store.getState().previousStep();
117
153
 
118
154
  // Android hardware back arrives via the <Modal>'s onRequestClose. Expose a
119
155
  // back-aware handler through `backRef` so that handler navigates a step back
120
156
  // when the flow can, instead of dismissing the whole SDK. It only reports
121
- // 'close' from the first step (consent) with close allowed; with
157
+ // 'close' from the opening step with close allowed; with
122
158
  // `disableClose` set it reports 'blocked' so back can never force the flow
123
159
  // closed. (iOS has no hardware back; its swipe-down keeps the standard
124
160
  // dismiss behaviour, handled by the Modal.)
@@ -152,22 +188,21 @@ export function KycFlow({
152
188
  country={headerCountry}
153
189
  onBack={onBack}
154
190
  onClose={onClose}
155
- // The searchable country picker pins its search box above a list that
156
- // can run to ~240 rows. Only the multi-country variant needs it: a
157
- // short flat list is happier in the normal scroll body.
191
+ // The searchable country picker (>5 countries) pins its search box
192
+ // above a ~240-row list; the address pin and entrance steps fill it
193
+ // too, since a map or panorama owns every touch and their actions
194
+ // ride StickyActions at the bottom of a BOUNDED body.
158
195
  fillsViewport={
159
- currentStep === 'country-select' &&
160
- countrySelectOptions({ config, serverConfig }).length > COUNTRY_SEARCH_THRESHOLD
196
+ (currentStep === 'country-select' &&
197
+ countrySelectOptions({ config, serverConfig }).length > COUNTRY_SEARCH_THRESHOLD) ||
198
+ currentStep === 'address-collection' ||
199
+ currentStep === 'address-entrance'
161
200
  }
162
- // A live camera asks for the whole screen: the sheet's header, padding
163
- // and scroll view are exactly what force a small viewfinder on a short
164
- // phone, and a camera you have to scroll to is a broken camera.
165
- //
166
- // Passed as a PROP rather than rendered as its own tree. The earlier
167
- // shape early-returned a different element tree, which changed the
168
- // step's parent MID-STEP — React unmounted and remounted it, wiping the
169
- // acknowledged primer and bouncing straight back to it. Scoped to the
170
- // step as well as the flag so the next step cannot inherit it.
201
+ // A live camera asks for the whole screen (a camera you have to scroll
202
+ // to is a broken camera). Passed as a PROP rather than rendered as its
203
+ // own tree: an early-returned tree changed the step's parent MID-STEP,
204
+ // so React remounted it and wiped the acknowledged primer. Scoped to
205
+ // the step as well as the flag so the next step cannot inherit it.
171
206
  immersive={immersiveCapture && currentStep === 'document-capture'}
172
207
  >
173
208
  <StepView step={currentStep} onClose={onClose} />
@@ -0,0 +1,100 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, View } from 'react-native';
3
+
4
+ import { radius } from '../config/theme';
5
+ import { useTheme } from './runtime';
6
+ import { MyazaText } from './Typography';
7
+ import { useReduceMotion } from './StaggerIn';
8
+
9
+ // ─── A text line that is on its way ─────────────────────────────────────────
10
+ //
11
+ // Drawn at the line's own height so the card around it does not move when the
12
+ // words land, and shaped like the answer (a rounded bar about the width of a
13
+ // short address) rather than a spinner: a spinner beside grey text says
14
+ // "busy", a bar where the text goes says "the line is coming" (user decision
15
+ // 2026-09-07). The pin summary and the review card use it while the reverse
16
+ // geocode is out. It pulses the way KeyPeoplePending's ghost roster does, so
17
+ // the SDK has one skeleton language, and holds still under reduced motion.
18
+ // The words still reach assistive tech through the live-region label.
19
+ //
20
+ // Mirrors the web SDK's components/LineSkeleton and Flutter's
21
+ // widgets/line_skeleton.dart.
22
+
23
+ type Variant = React.ComponentProps<typeof MyazaText>['variant'];
24
+
25
+ export function LineSkeleton({
26
+ label,
27
+ variant = 'bodyMedium',
28
+ textStyle,
29
+ barHeight = 10,
30
+ width = '62%',
31
+ style,
32
+ }: {
33
+ /** What a screen reader hears, e.g. "Finding the address…". */
34
+ label: string;
35
+ /** The variant the real line renders with, so the ghost owns its height. */
36
+ variant?: Variant;
37
+ textStyle?: object;
38
+ barHeight?: number;
39
+ /** How much of the line the bar covers; an address, not a paragraph. */
40
+ width?: number | `${number}%`;
41
+ style?: object;
42
+ }): React.ReactElement {
43
+ const { colors } = useTheme();
44
+ const reduceMotion = useReduceMotion();
45
+ const pulse = useRef(new Animated.Value(1)).current;
46
+
47
+ useEffect(() => {
48
+ if (reduceMotion) return undefined;
49
+ const loop = Animated.loop(
50
+ Animated.sequence([
51
+ Animated.timing(pulse, { toValue: 0.45, duration: 600, useNativeDriver: true }),
52
+ Animated.timing(pulse, { toValue: 1, duration: 600, useNativeDriver: true }),
53
+ ]),
54
+ );
55
+ loop.start();
56
+ return () => loop.stop();
57
+ }, [pulse, reduceMotion]);
58
+
59
+ return (
60
+ <View
61
+ accessible
62
+ accessibilityLiveRegion="polite"
63
+ accessibilityLabel={label}
64
+ style={[{ justifyContent: 'center' }, style]}
65
+ >
66
+ {/* An invisible line in the real variant owns the height, so the card
67
+ does not move when the words replace the bar. */}
68
+ <MyazaText variant={variant} style={[textStyle, { opacity: 0 }]} numberOfLines={1}>
69
+ {' '}
70
+ </MyazaText>
71
+ <Animated.View
72
+ pointerEvents="none"
73
+ style={{
74
+ position: 'absolute',
75
+ left: 0,
76
+ width,
77
+ height: barHeight,
78
+ borderRadius: radius.full,
79
+ backgroundColor: colors.primary100,
80
+ opacity: reduceMotion ? 0.7 : pulse,
81
+ }}
82
+ />
83
+ </View>
84
+ );
85
+ }
86
+
87
+ /** The resolved line fading in where the skeleton was, so the swap reads as
88
+ * the answer landing rather than a flicker. Instant under reduced motion. */
89
+ export function LineReveal({ children, style }: { children: React.ReactNode; style?: object }): React.ReactElement {
90
+ const reduceMotion = useReduceMotion();
91
+ const opacity = useRef(new Animated.Value(0)).current;
92
+ useEffect(() => {
93
+ if (reduceMotion) {
94
+ opacity.setValue(1);
95
+ return;
96
+ }
97
+ Animated.timing(opacity, { toValue: 1, duration: 200, useNativeDriver: true }).start();
98
+ }, [opacity, reduceMotion]);
99
+ return <Animated.View style={[{ opacity }, style]}>{children}</Animated.View>;
100
+ }
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import { Pressable, View } from 'react-native';
3
+
4
+ import { radius } from '../config/theme';
5
+ import { useTheme } from './runtime';
6
+ import { Icon } from './Icon';
7
+ import { MyazaText } from './Typography';
8
+
9
+ // The map's furniture, split from MapPinPicker per the 200-line rule.
10
+
11
+ /** Zoom controls, top-right. Absent on a read-only summary map. */
12
+ export function MapZoomControls({ onZoom }: { onZoom: (delta: number) => void }): React.ReactElement {
13
+ const { colors } = useTheme();
14
+ return (
15
+ <View
16
+ style={{
17
+ position: 'absolute',
18
+ right: 8,
19
+ top: 8,
20
+ borderRadius: radius.sm,
21
+ borderWidth: 1,
22
+ borderColor: colors.border,
23
+ backgroundColor: colors.background,
24
+ overflow: 'hidden',
25
+ }}
26
+ >
27
+ <Pressable
28
+ accessibilityRole="button"
29
+ accessibilityLabel="Zoom in"
30
+ onPress={() => onZoom(1)}
31
+ style={{ padding: 8 }}
32
+ >
33
+ <Icon name="plus" size={16} color={colors.textDark} />
34
+ </Pressable>
35
+ <Pressable
36
+ accessibilityRole="button"
37
+ accessibilityLabel="Zoom out"
38
+ onPress={() => onZoom(-1)}
39
+ style={{ padding: 8, borderTopWidth: 1, borderTopColor: colors.border }}
40
+ >
41
+ <Icon name="minus" size={16} color={colors.textDark} />
42
+ </Pressable>
43
+ </View>
44
+ );
45
+ }
46
+
47
+ /** OSM tile-usage terms require this to be visible wherever the tiles are. */
48
+ export function MapAttribution(): React.ReactElement {
49
+ const { colors } = useTheme();
50
+ return (
51
+ <View
52
+ pointerEvents="none"
53
+ style={{
54
+ position: 'absolute',
55
+ right: 6,
56
+ bottom: 4,
57
+ paddingHorizontal: 4,
58
+ borderRadius: 3,
59
+ backgroundColor: colors.background + 'B3',
60
+ }}
61
+ >
62
+ <MyazaText variant="bodySmall" style={{ fontSize: 10 }} color={colors.textSecondary}>
63
+ © OpenStreetMap contributors
64
+ </MyazaText>
65
+ </View>
66
+ );
67
+ }
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import Svg, { Circle, Rect } from 'react-native-svg';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // The Myaza map pin — the same drawing as the web SDK's MapPinMarker and
7
+ // Flutter's map_pin_marker.dart, so the built-in OSM picker wears the pin the
8
+ // framed Google map already does. A 1:1 copy of the Bolt reference: head 46px,
9
+ // eye 0.31, stem 0.098 wide showing 0.41 below the head, no casings, flat body
10
+ // (no shadow on the pin itself).
11
+ //
12
+ // The GROUND DOT is the drag-state shadow: while the map pans the pin lifts
13
+ // 17px and the dot appears at the landing point beneath it; when the pin
14
+ // lands the dot goes, leaving the stem tip resting exactly on the picked
15
+ // coordinate. The parent aligns this widget's BOTTOM edge to the map centre.
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export const PIN_LIFT = 17;
19
+
20
+ export function MapPinMarker({
21
+ lifted,
22
+ color,
23
+ }: {
24
+ /** The map is mid-pan: the pin floats and the landing dot shows. */
25
+ lifted: boolean;
26
+ /** The workflow's primary colour (the whole pin wears it). */
27
+ color: string;
28
+ }): React.ReactElement {
29
+ return (
30
+ <View style={{ alignItems: 'center' }} pointerEvents="none">
31
+ <View
32
+ style={{
33
+ position: 'absolute',
34
+ bottom: -2.5,
35
+ width: 11,
36
+ height: 5,
37
+ borderRadius: 3,
38
+ backgroundColor: 'rgba(7, 3, 48, 0.5)',
39
+ opacity: lifted ? 1 : 0,
40
+ }}
41
+ />
42
+ <Svg
43
+ width={46}
44
+ height={65}
45
+ viewBox="0 0 46 65"
46
+ style={{ transform: [{ translateY: lifted ? -PIN_LIFT : 0 }] }}
47
+ >
48
+ <Rect x={20.75} y={42} width={4.5} height={23} rx={2.25} fill={color} />
49
+ <Circle cx={23} cy={23} r={23} fill={color} />
50
+ <Circle cx={23} cy={23} r={7} fill="#FFFFFF" />
51
+ </Svg>
52
+ </View>
53
+ );
54
+ }