@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
@@ -0,0 +1,176 @@
1
+ import {
2
+ Info, BadgeCheck, BookUser, Building2, Stamp, Calendar, Camera, CameraOff, ChevronDown,
3
+ ChevronLeft, ChevronRight, Car, Check, CircleAlert, Contact, CreditCard, Fingerprint,
4
+ FileText, FlaskConical, IdCard, Image as ImageIcon, Landmark, Lightbulb, LocateFixed, MapPin,
5
+ MapPinHouse, MapPinCheck, MapPinned, Radar, BellRing, SlidersHorizontal, Minus, Globe, House,
6
+ Mail, Maximize2, Lock, MessageCircle, MessageSquare, Moon, MoveLeft, Nfc, Filter, Pencil,
7
+ PencilLine, Smartphone, RefreshCw, RefreshCcw, ScanFace, ScanLine, Search, Zap, ZapOff,
8
+ CircleDashed, CircleCheck, CircleHelp, CircleX, ShieldCheck, Sun, Timer, Upload, Plus,
9
+ UserRound, UserRoundPlus, Link2, Copy, Share2, UsersRound, VideoOff, Vote, X,
10
+ type LucideIcon
11
+ } from 'lucide-react-native';
12
+
13
+ // Centralised icon set — the SAME Lucide icons the Flutter SDK uses
14
+ // (`lucide_icons_flutter`) and the web SDK uses (`lucide-react`), so the
15
+ // glyphs are pixel-identical across all three platforms. The component that
16
+ // draws them is Icon.tsx (200-line split).
17
+
18
+ export type IconName =
19
+ // chrome
20
+ | 'close'
21
+ | 'back'
22
+ | 'moon'
23
+ | 'sun'
24
+ | 'check'
25
+ | 'lock'
26
+ | 'alert'
27
+ | 'flask'
28
+ | 'refresh'
29
+ | 'refresh-ccw'
30
+ | 'maximize'
31
+ | 'calendar'
32
+ | 'image'
33
+ | 'chevron-left'
34
+ | 'chevron-right'
35
+ | 'chevron-down'
36
+ // consent hero + process steps
37
+ | 'shield'
38
+ | 'badge-check'
39
+ | 'circle-dashed'
40
+ | 'circle-check'
41
+ | 'circle-help'
42
+ | 'circle-x'
43
+ | 'building-2'
44
+ | 'stamp'
45
+ | 'user'
46
+ | 'user-plus'
47
+ | 'plus'
48
+ | 'info'
49
+ | 'users'
50
+ | 'scan-line'
51
+ | 'scan-face'
52
+ | 'nfc'
53
+ | 'filter'
54
+ | 'pencil'
55
+ | 'pencil-line'
56
+ | 'search'
57
+ | 'zap'
58
+ | 'link'
59
+ | 'copy'
60
+ | 'share'
61
+ | 'zap-off'
62
+ | 'timer'
63
+ // id-type glyphs
64
+ | 'fingerprint'
65
+ | 'credit-card'
66
+ | 'globe'
67
+ | 'car'
68
+ | 'vote'
69
+ | 'file-text'
70
+ | 'house'
71
+ | 'landmark'
72
+ | 'id-card'
73
+ | 'contact'
74
+ | 'passport'
75
+ | 'camera'
76
+ | 'camera-off'
77
+ | 'video-off'
78
+ | 'upload'
79
+ | 'x'
80
+ // address collection (map picker + the presence milestone track)
81
+ | 'map-pin'
82
+ | 'map-pin-house'
83
+ | 'map-pin-check'
84
+ | 'map-pinned'
85
+ | 'radar'
86
+ | 'bell-ring'
87
+ | 'sliders'
88
+ | 'locate'
89
+ | 'minus'
90
+ | 'lightbulb'
91
+ // contact verification (OTP channels + step footer)
92
+ | 'mail'
93
+ | 'smartphone'
94
+ | 'message-square'
95
+ | 'message-circle';
96
+
97
+ // Maps each name to its Lucide component. Names mirror the Flutter SDK exactly:
98
+ // shieldCheck · badgeCheck · userRound · scanLine · scanFace · fingerprint ·
99
+ // creditCard · globe · car · vote · fileText (+ chrome: X / ArrowLeft /
100
+ // Moon / Sun / Check / Lock / CircleAlert / RefreshCw / Camera).
101
+ export const ICONS: Record<IconName, LucideIcon> = {
102
+ close: X,
103
+ back: MoveLeft, // longer-shaft left arrow, matching Flutter's keyboard_backspace
104
+ moon: Moon,
105
+ sun: Sun,
106
+ check: Check,
107
+ lock: Lock,
108
+ alert: CircleAlert,
109
+ // The environment banner's mark, shared with the web SDK (same lucide glyph)
110
+ // and mirrored by Flutter's Icons.science_outlined.
111
+ flask: FlaskConical,
112
+ refresh: RefreshCw,
113
+ 'refresh-ccw': RefreshCcw,
114
+ maximize: Maximize2,
115
+ calendar: Calendar,
116
+ image: ImageIcon,
117
+ 'chevron-left': ChevronLeft,
118
+ 'chevron-right': ChevronRight,
119
+ 'chevron-down': ChevronDown,
120
+ shield: ShieldCheck,
121
+ 'badge-check': BadgeCheck,
122
+ 'circle-dashed': CircleDashed,
123
+ 'circle-check': CircleCheck,
124
+ 'circle-help': CircleHelp,
125
+ 'circle-x': CircleX,
126
+ 'building-2': Building2,
127
+ stamp: Stamp,
128
+ user: UserRound,
129
+ info: Info,
130
+ 'user-plus': UserRoundPlus,
131
+ plus: Plus,
132
+ link: Link2,
133
+ copy: Copy,
134
+ share: Share2,
135
+ users: UsersRound,
136
+ 'scan-line': ScanLine,
137
+ 'scan-face': ScanFace,
138
+ nfc: Nfc,
139
+ filter: Filter,
140
+ pencil: Pencil,
141
+ 'pencil-line': PencilLine,
142
+ search: Search,
143
+ zap: Zap,
144
+ 'zap-off': ZapOff,
145
+ timer: Timer,
146
+ fingerprint: Fingerprint,
147
+ 'credit-card': CreditCard,
148
+ globe: Globe,
149
+ car: Car,
150
+ vote: Vote,
151
+ 'file-text': FileText,
152
+ house: House,
153
+ landmark: Landmark,
154
+ 'map-pin': MapPin,
155
+ 'map-pin-house': MapPinHouse,
156
+ 'map-pin-check': MapPinCheck,
157
+ 'map-pinned': MapPinned,
158
+ radar: Radar,
159
+ 'bell-ring': BellRing,
160
+ sliders: SlidersHorizontal,
161
+ locate: LocateFixed,
162
+ minus: Minus,
163
+ 'id-card': IdCard,
164
+ contact: Contact,
165
+ passport: BookUser,
166
+ camera: Camera,
167
+ 'camera-off': CameraOff,
168
+ 'video-off': VideoOff,
169
+ upload: Upload,
170
+ x: X,
171
+ lightbulb: Lightbulb,
172
+ mail: Mail,
173
+ smartphone: Smartphone,
174
+ 'message-square': MessageSquare,
175
+ 'message-circle': MessageCircle,
176
+ };
@@ -1,10 +1,12 @@
1
1
  import { ID_TYPES } from '../config/idTypes';
2
- import { poaMaxAgeDays } from '../config/proofOfAddress';
2
+ import { poaDocumentTypes, poaMaxAgeDays, poaNamePolicy } from '../config/proofOfAddress';
3
3
  import { documentCaptureMeta } from '../screens/DocumentCaptureStep';
4
+ import { documentCaptureMethods } from '../config/documentCaptureMethods';
4
5
  import { questionnaireMeta } from '../screens/QuestionnaireStep';
5
6
  import { contactCodeLength, contactMeta, type ContactChallenge } from '../config/contact';
6
7
 
7
8
  import { proofOfAddressMeta } from '../screens/ProofOfAddressStep';
9
+ import { addressStepMeta } from '../screens/address';
8
10
  import { countrySelectMeta } from '../screens/CountrySelectStep';
9
11
  import { businessDetailsMeta } from '../screens/BusinessDetailsStep';
10
12
  import { businessKeyPeopleMeta } from '../screens/BusinessKeyPeopleStep';
@@ -34,13 +36,36 @@ export interface StepHeaderContext {
34
36
  documentCapturePhase: DocumentCapturePhase;
35
37
  /** Outstanding contact code — lets the header describe the OTP in flight. */
36
38
  contactChallenge: ContactChallenge | null;
39
+ /**
40
+ * The presence primer is on screen in place of the address step's body.
41
+ *
42
+ * It carries its own title, so the step's would sit above it saying something
43
+ * else about a screen that is not showing. Blanking the header leaves just
44
+ * the back arrow, the way the consent step does.
45
+ */
46
+ addressIntroPending?: boolean;
47
+ /** The entrance step is framing street imagery rather than asking for a photo. */
48
+ addressEntranceFraming?: boolean;
49
+ /** The proof-of-address kind the applicant has picked, so the header can ask
50
+ * for what the workflow's name rule wants on THAT document. */
51
+ poaDocumentType?: string | null;
37
52
  }
38
53
 
39
54
  export function stepHeaderMeta(
40
55
  currentStep: KYCStep,
41
- { config, country, selectedIdType, documentCapturePhase, contactChallenge }: StepHeaderContext,
56
+ {
57
+ config,
58
+ country,
59
+ selectedIdType,
60
+ documentCapturePhase,
61
+ contactChallenge,
62
+ addressIntroPending,
63
+ addressEntranceFraming,
64
+ poaDocumentType,
65
+ }: StepHeaderContext,
42
66
  ): { title: string; description: string | null } {
43
67
  const label = labelFor(country, selectedIdType);
68
+ if (addressIntroPending) return { title: '', description: null };
44
69
  switch (currentStep) {
45
70
  case 'consent':
46
71
  return { title: '', description: null as string | null };
@@ -53,7 +78,11 @@ export function stepHeaderMeta(
53
78
  case 'document-capture':
54
79
  // Phase-aware title/description live in the header (synced from the
55
80
  // capture screen via `documentCapturePhase`) — mirrors Flutter.
56
- return documentCaptureMeta(documentCapturePhase, label);
81
+ return documentCaptureMeta(
82
+ documentCapturePhase,
83
+ label,
84
+ documentCaptureMethods(config).scan ? 'scan' : 'upload',
85
+ );
57
86
  case 'liveness':
58
87
  return { title: 'Face Verification', description: 'Follow the on-screen instructions' };
59
88
  case 'questionnaire':
@@ -68,8 +97,22 @@ export function stepHeaderMeta(
68
97
  codeLength: contactCodeLength(config.phoneVerification),
69
98
  challenge: contactChallenge,
70
99
  });
71
- case 'proof-of-address':
72
- return proofOfAddressMeta(poaMaxAgeDays(config.proofOfAddress));
100
+ case 'proof-of-address': {
101
+ // Word the ask by the rule the server will judge THIS document under:
102
+ // the picked kind, else the first the country offers (what the screen
103
+ // preselects). Under `off` the header stops asking for the name.
104
+ const poa = config.proofOfAddress;
105
+ const kind = poaDocumentType ?? poaDocumentTypes(poa, country)[0];
106
+ return proofOfAddressMeta(
107
+ poaMaxAgeDays(poa),
108
+ poaNamePolicy(poa, country, kind as Parameters<typeof poaNamePolicy>[2]) !== 'off',
109
+ );
110
+ }
111
+ case 'address-search':
112
+ case 'address-collection':
113
+ case 'address-entrance':
114
+ case 'address-review':
115
+ return addressStepMeta(currentStep, config.subjectType === 'business', { framing: addressEntranceFraming });
73
116
  case 'country-select':
74
117
  return countrySelectMeta;
75
118
  case 'business-details':
@@ -0,0 +1,125 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Address Intelligence — the pure half.
3
+ //
4
+ // A smart address is a map pin (+ optional door photo and directions) the
5
+ // server corroborates against the evidence it already holds. The result is a
6
+ // SOFT sub-result: it never changes the verification's own status, it feeds
7
+ // decisioning. So nothing here should block a user beyond what the config
8
+ // explicitly requires (`requirePin`, required photo/directions).
9
+ //
10
+ // Mirrors the web SDK's address handling — keep the two in lockstep.
11
+ // ---------------------------------------------------------------------------
12
+
13
+ import { addressFlowOptions, type AddressFlowOptions } from '../lib/address-flow';
14
+ import { requiredPrefillSubmission } from '../lib/address-field-modes';
15
+ import { isBusinessFlow } from './business';
16
+ import type { SubjectType, WorkflowBusinessConfig } from '../types/business';
17
+ import type { AddressCollectionConfig } from '../types/workflow';
18
+ import type { AddressState } from '../store/state';
19
+ import type { VerifyRequest } from '../services/api-verify-types';
20
+
21
+ export type AddressFieldMode = 'off' | 'optional' | 'required';
22
+
23
+ /** Whether the step is part of the flow. */
24
+ export function hasAddressCollectionStep(
25
+ address: AddressCollectionConfig | undefined | null,
26
+ ): boolean {
27
+ return address?.enabled === true;
28
+ }
29
+
30
+ /**
31
+ * Which address screens this flow has — ONE call, read by the step-order
32
+ * builder AND by the address steps themselves, so the two can never disagree
33
+ * about which screens exist.
34
+ *
35
+ * KYB collapses to the single premises step: the pin plus its directions ARE
36
+ * the capture, and the business flow already has its own section rhythm.
37
+ */
38
+ export function addressFlowFor(
39
+ config: {
40
+ subjectType?: SubjectType;
41
+ business?: WorkflowBusinessConfig;
42
+ addressCollection?: AddressCollectionConfig;
43
+ },
44
+ /** The server offers a forward-search backend. */
45
+ serverSearch: boolean,
46
+ /** SANDBOX keys stub the vendor surfaces (addressVendorsStubbed). */
47
+ stubbed = false,
48
+ /** A maps frame URL this install can render (a WebView is installed): the
49
+ * framed street-view page carries the panorama for it. */
50
+ hasStreetViewFrame = false,
51
+ ): AddressFlowOptions {
52
+ if (isBusinessFlow(config)) {
53
+ return { searchAvailable: false, photoMode: 'off', streetViewOffered: false };
54
+ }
55
+ return addressFlowOptions({
56
+ photo: config.addressCollection?.photo,
57
+ streetView: config.addressCollection?.streetView,
58
+ serverSearch: serverSearch && !stubbed,
59
+ // The builder's live preview and the in-document Google key are web-only
60
+ // surfaces, so neither flag can ever be true here. Passing them
61
+ // explicitly keeps the rule a mirror.
62
+ previewMode: false,
63
+ hasGoogleKey: false,
64
+ hasStreetViewFrame,
65
+ });
66
+ }
67
+
68
+ // The old single-screen Continue gate (canContinueAddress and the two mode
69
+ // helpers that fed it) was deleted with the four-step flow: each step now
70
+ // gates itself, and a helper stating the old combined rule is a trap for
71
+ // whoever wires it back up. The Flutter port removed its copies for the same
72
+ // reason.
73
+
74
+ /**
75
+ * The wire block. `config` is the flow's address step: a workflow-REQUIRED
76
+ * field the applicant left on its map prefill rides that prefill (they saw
77
+ * it filled and confirmed by continuing, and the server 422s a required
78
+ * field that never arrives). Session progress passes no config and stays
79
+ * raw, so a resume never confuses a prefill with a claim.
80
+ */
81
+ export function addressPayload(
82
+ address: AddressState,
83
+ config?: AddressCollectionConfig | null,
84
+ ): NonNullable<VerifyRequest['address']> {
85
+ return {
86
+ lat: address.lat,
87
+ lng: address.lng,
88
+ // The line the applicant CONFIRMED: the server prefers it over its own
89
+ // reverse geocode, whose coverage drops whole streets in our markets.
90
+ ...(address.label?.trim() ? { label: address.label.trim() } : {}),
91
+ ...(typeof address.accuracy === 'number' ? { accuracy: address.accuracy } : {}),
92
+ ...(address.directions.trim() ? { directions: address.directions.trim() } : {}),
93
+ ...(address.propertyName.trim() ? { propertyName: address.propertyName.trim() } : {}),
94
+ ...(address.propertyNumber.trim() ? { propertyNumber: address.propertyNumber.trim() } : {}),
95
+ ...(address.street?.trim() ? { street: address.street.trim() } : {}),
96
+ // The rest of the edit-details form — claims, sent only when typed.
97
+ ...(address.unit?.trim() ? { unit: address.unit.trim() } : {}),
98
+ ...(address.neighbourhood?.trim() ? { neighbourhood: address.neighbourhood.trim() } : {}),
99
+ ...(address.city?.trim() ? { city: address.city.trim() } : {}),
100
+ ...(address.state?.trim() ? { state: address.state.trim() } : {}),
101
+ ...(address.postcode?.trim() ? { postcode: address.postcode.trim() } : {}),
102
+ // Required-but-untouched prefills, LAST among the typed fields: the helper
103
+ // never carries a typed value, so nothing above is overridden.
104
+ ...requiredPrefillSubmission(config, address),
105
+ ...(address.streetView ? { streetView: address.streetView } : {}),
106
+ ...(typeof address.deviceLat === 'number' && typeof address.deviceLng === 'number'
107
+ ? {
108
+ deviceLat: address.deviceLat,
109
+ deviceLng: address.deviceLng,
110
+ ...(typeof address.deviceAccuracy === 'number'
111
+ ? { deviceAccuracy: address.deviceAccuracy }
112
+ : {}),
113
+ ...(address.capturedAt ? { capturedAt: address.capturedAt } : {}),
114
+ }
115
+ : {}),
116
+ };
117
+ }
118
+
119
+ /** Accepted door-photo types — an image, never a PDF. */
120
+ export const ADDRESS_PHOTO_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp'] as const;
121
+
122
+ export function isAcceptedAddressPhotoMimeType(mimeType: string | undefined): boolean {
123
+ const base = (mimeType?.split(';')[0] ?? '').trim().toLowerCase();
124
+ return (ADDRESS_PHOTO_MIME_TYPES as readonly string[]).includes(base);
125
+ }
@@ -0,0 +1,100 @@
1
+ import { configScope } from '../lib/scope';
2
+
3
+ // ─── The biometric scopes' flow options ─────────────────────────────────────
4
+ //
5
+ // Mirrors the server's lib/workflows/biometric-options.ts; keep the defaults
6
+ // in lockstep. All three are UX policy this SDK enforces (user decision
7
+ // 2026-09-07, React Native first). A published workflow sets them in the
8
+ // builder's Presence Intelligence panel; a prop-configured mount passes the
9
+ // same `biometric` block.
10
+ //
11
+ // selfieReview Show the captured selfie with Retake and Continue before
12
+ // submitting. OFF by default on both biometric scopes: a
13
+ // re-authentication is a few-second check, and a review
14
+ // screen is a stop in the middle of it. The liveness step
15
+ // hands straight over once the ring has closed.
16
+ // resultDelivery WHERE the verdict lands. 'both' (the default) and 'app'
17
+ // both hold the person on one loading screen from the
18
+ // shutter to the verdict, polling the publishable status
19
+ // endpoint until the check settles, because a
20
+ // re-authentication is answered NOW or it is useless; they
21
+ // differ only on the SERVER, which sends no webhook for an
22
+ // 'app' check. 'webhook' is the fire-and-forget model every
23
+ // other flow runs. Enrolment has no verdict to deliver, so
24
+ // it never waits.
25
+ // doneButton Whether the final screen carries a Done button (default
26
+ // ON). Off when the host app dismisses the flow itself from
27
+ // `onResult` (or `onSubmit` on a webhook delivery), so the
28
+ // person is never shown a button the app is about to act
29
+ // for. The screen then stays until the host closes the SDK.
30
+
31
+ /** One screen's words. A field absent (or blank) means the SDK's default
32
+ * shows. `{firstName}` / `{lastName}` tokens fill from `userData`, exactly as
33
+ * the consent and success copy do. */
34
+ export interface BiometricCopyText {
35
+ title?: string;
36
+ description?: string;
37
+ }
38
+
39
+ /**
40
+ * The org's own words on the biometric screens (the builder's "Face check
41
+ * screens" fields; user decision 2026-09-07). `waiting` is the ONE loading
42
+ * screen from the shutter to the verdict, on both scopes; `verified` and
43
+ * `declined` are the in-flow verdict screens, so they apply to a
44
+ * re-authentication only (publish refuses them on enrolment, which shows no
45
+ * verdict). A `declined` description wins over the server's reason: the org
46
+ * chose to say that. Resolved for rendering by lib/biometric-copy.ts.
47
+ */
48
+ export interface BiometricCopy {
49
+ waiting?: BiometricCopyText;
50
+ verified?: BiometricCopyText;
51
+ declined?: BiometricCopyText;
52
+ }
53
+
54
+ export interface BiometricFlowConfig {
55
+ selfieReview?: boolean;
56
+ resultDelivery?: 'app' | 'webhook' | 'both';
57
+ doneButton?: boolean;
58
+ copy?: BiometricCopy;
59
+ }
60
+
61
+ export interface BiometricFlowOptions {
62
+ selfieReview: boolean;
63
+ /** Null on enrolment: nothing is delivered, so there is nothing to wait for. */
64
+ resultDelivery: 'app' | 'webhook' | 'both' | null;
65
+ doneButton: boolean;
66
+ }
67
+
68
+ type BiometricConfigLike = { scope?: string; biometric?: BiometricFlowConfig | null };
69
+
70
+ /** The effective options, or null when the flow is not a biometric scope. */
71
+ export function biometricFlowOptions(config: BiometricConfigLike): BiometricFlowOptions | null {
72
+ const scope = configScope(config);
73
+ if (scope !== 'biometric-authentication' && scope !== 'biometric-enrollment') return null;
74
+ const block = config.biometric ?? {};
75
+ return {
76
+ selfieReview: block.selfieReview ?? false,
77
+ resultDelivery: scope === 'biometric-authentication' ? (block.resultDelivery ?? 'both') : null,
78
+ doneButton: block.doneButton ?? true,
79
+ };
80
+ }
81
+
82
+ /** Whether the liveness step shows the selfie review before handing over. A
83
+ * full verification always does; only the biometric scopes can switch it off. */
84
+ export function showsSelfieReview(config: BiometricConfigLike): boolean {
85
+ return biometricFlowOptions(config)?.selfieReview ?? true;
86
+ }
87
+
88
+ /** Whether the submitted step waits for the verdict in the flow ('app' and
89
+ * 'both') rather than leaving it to the webhook. Only a re-authentication
90
+ * ever does. */
91
+ export function waitsForResult(config: BiometricConfigLike): boolean {
92
+ const delivery = biometricFlowOptions(config)?.resultDelivery;
93
+ return delivery === 'app' || delivery === 'both';
94
+ }
95
+
96
+ /** Whether the final screen carries a Done button. Always on off the
97
+ * biometric scopes; only they can hide it. */
98
+ export function showsDoneButton(config: BiometricConfigLike): boolean {
99
+ return biometricFlowOptions(config)?.doneButton ?? true;
100
+ }
@@ -36,6 +36,20 @@ export const BUSINESS_PRODUCTS: readonly BusinessProductDef[] = [
36
36
  inputLabel: 'Registration number',
37
37
  placeholder: 'e.g. RC0000000',
38
38
  },
39
+ {
40
+ key: 'business-address',
41
+ label: 'Business Address',
42
+ inputLabel: 'Registration number',
43
+ placeholder: 'e.g. 201133333323',
44
+ countries: ['ZA'],
45
+ },
46
+ {
47
+ key: 'business-filings',
48
+ label: 'Company Filings',
49
+ inputLabel: 'Registration number',
50
+ placeholder: 'e.g. CI-ABJ-03-2023-B17-00120',
51
+ countries: ['CI'],
52
+ },
39
53
  {
40
54
  key: 'business-tax',
41
55
  label: 'Business + Tax ID',
@@ -91,7 +105,12 @@ export function businessProductsForCountry(
91
105
  ): string[] {
92
106
  const offered = businessProductsFor(business).filter((key) => {
93
107
  const def = BUSINESS_PRODUCTS.find((p) => p.key === key);
94
- return !def?.countries || def.countries.includes(country);
108
+ // A key this build does not know is HIDDEN, never offered everywhere: the
109
+ // server rejects a wrong-country pick at verify (product_unsupported), so
110
+ // over-offering dead-ends the applicant, while hiding degrades to the
111
+ // standard lookup until the SDK updates.
112
+ if (!def) return false;
113
+ return !def.countries || def.countries.includes(country);
95
114
  });
96
115
  return offered.length > 0 ? offered : [DEFAULT_BUSINESS_PRODUCT];
97
116
  }
@@ -87,8 +87,14 @@ export type BusinessSectionStep =
87
87
  export function businessSectionSteps(
88
88
  business: WorkflowBusinessConfig | undefined,
89
89
  withQuestionnaire = false,
90
- ): (BusinessSectionStep | 'questionnaire')[] {
91
- const steps: (BusinessSectionStep | 'questionnaire')[] = ['business-details'];
90
+ withAddressCollection = false,
91
+ ): (BusinessSectionStep | 'questionnaire' | 'address-collection')[] {
92
+ const steps: (BusinessSectionStep | 'questionnaire' | 'address-collection')[] = [
93
+ 'business-details',
94
+ ];
95
+ // The premises pin follows the company details it is about — before the
96
+ // paperwork, and long before the application hands over to other people.
97
+ if (withAddressCollection) steps.push('address-collection');
92
98
  // Documents BEFORE key people: they are about the company the applicant has
93
99
  // just identified, so they follow that thread, and the register's officer
94
100
  // list - which the key-people step is a confirmation of - is what should
@@ -0,0 +1,19 @@
1
+ import type { KYCStep } from '../types/config';
2
+
3
+ // ─── The consent screen switch (`consentStep`) ──────────────────────────────
4
+ //
5
+ // A workflow can switch the opening consent (welcome) screen off, for a host
6
+ // app that has already asked: the flow then opens on its first real step.
7
+ // Mirrors the server's `WorkflowConfigSchema.consentStep` and the web /
8
+ // Flutter helpers of the same name; keep the default in lockstep.
9
+
10
+ /** Whether the flow opens on the consent screen. Absent = yes. */
11
+ export function hasConsentStep(config: { consentStep?: boolean | null }): boolean {
12
+ return config.consentStep !== false;
13
+ }
14
+
15
+ /** The step a built order opens on; 'consent' for an empty order, which no
16
+ * flow produces. */
17
+ export function openingStepOf(order: readonly KYCStep[]): KYCStep {
18
+ return order[0] ?? 'consent';
19
+ }
@@ -0,0 +1,31 @@
1
+ // ---------------------------------------------------------------------------
2
+ // How the applicant may put a document in front of us.
3
+ //
4
+ // Two workflow switches, each on unless set to `false`: `allowDocumentScan`
5
+ // (photograph it with the live camera) and `allowDocumentUpload` (choose a
6
+ // photo of it from the device). The server refuses to publish a workflow with
7
+ // both off, but a stored or prop-supplied config can still carry that, and a
8
+ // document step with no way in is a dead end. So the camera comes back on in
9
+ // that case: it is the method the step was built around.
10
+ //
11
+ // Screens read the pair through here and never the raw keys, so the fallback
12
+ // is one rule rather than a check every screen has to remember. Mirrors the
13
+ // web and Flutter SDKs.
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export interface DocumentCaptureMethods {
17
+ /** Photograph the document with the live camera. */
18
+ scan: boolean;
19
+ /** Choose a photo of the document from the device. */
20
+ upload: boolean;
21
+ }
22
+
23
+ export function documentCaptureMethods(config: {
24
+ allowDocumentScan?: boolean;
25
+ allowDocumentUpload?: boolean;
26
+ }): DocumentCaptureMethods {
27
+ const upload = config.allowDocumentUpload !== false;
28
+ // With upload off the camera is the only way left, whatever its flag says.
29
+ const scan = upload ? config.allowDocumentScan !== false : true;
30
+ return { scan, upload };
31
+ }