@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,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,11 @@
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
4
  import { questionnaireMeta } from '../screens/QuestionnaireStep';
5
5
  import { contactCodeLength, contactMeta, type ContactChallenge } from '../config/contact';
6
6
 
7
7
  import { proofOfAddressMeta } from '../screens/ProofOfAddressStep';
8
+ import { addressStepMeta } from '../screens/address';
8
9
  import { countrySelectMeta } from '../screens/CountrySelectStep';
9
10
  import { businessDetailsMeta } from '../screens/BusinessDetailsStep';
10
11
  import { businessKeyPeopleMeta } from '../screens/BusinessKeyPeopleStep';
@@ -34,20 +35,45 @@ export interface StepHeaderContext {
34
35
  documentCapturePhase: DocumentCapturePhase;
35
36
  /** Outstanding contact code — lets the header describe the OTP in flight. */
36
37
  contactChallenge: ContactChallenge | null;
38
+ /**
39
+ * The presence primer is on screen in place of the address step's body.
40
+ *
41
+ * It carries its own title, so the step's would sit above it saying something
42
+ * else about a screen that is not showing. Blanking the header leaves just
43
+ * the back arrow, the way the consent step does.
44
+ */
45
+ addressIntroPending?: boolean;
46
+ /** The entrance step is framing street imagery rather than asking for a photo. */
47
+ addressEntranceFraming?: boolean;
48
+ /** The proof-of-address kind the applicant has picked, so the header can ask
49
+ * for what the workflow's name rule wants on THAT document. */
50
+ poaDocumentType?: string | null;
37
51
  }
38
52
 
39
53
  export function stepHeaderMeta(
40
54
  currentStep: KYCStep,
41
- { config, country, selectedIdType, documentCapturePhase, contactChallenge }: StepHeaderContext,
55
+ {
56
+ config,
57
+ country,
58
+ selectedIdType,
59
+ documentCapturePhase,
60
+ contactChallenge,
61
+ addressIntroPending,
62
+ addressEntranceFraming,
63
+ poaDocumentType,
64
+ }: StepHeaderContext,
42
65
  ): { title: string; description: string | null } {
43
66
  const label = labelFor(country, selectedIdType);
67
+ if (addressIntroPending) return { title: '', description: null };
44
68
  switch (currentStep) {
45
69
  case 'consent':
46
70
  return { title: '', description: null as string | null };
47
71
  case 'id-type':
48
72
  return { title: 'Select ID Type', description: "Choose the type of identification document you'd like to use." };
49
73
  case 'id-input':
50
- return { title: 'Enter Your Details', description: `Provide your ${label} for verification.` };
74
+ // The number is all this step asks for. The name comes from the
75
+ // integrator (mount props / the session), never the applicant.
76
+ return { title: `Enter your ${label}`, description: 'We’ll check this against the official record.' };
51
77
  case 'document-capture':
52
78
  // Phase-aware title/description live in the header (synced from the
53
79
  // capture screen via `documentCapturePhase`) — mirrors Flutter.
@@ -66,8 +92,22 @@ export function stepHeaderMeta(
66
92
  codeLength: contactCodeLength(config.phoneVerification),
67
93
  challenge: contactChallenge,
68
94
  });
69
- case 'proof-of-address':
70
- return proofOfAddressMeta(poaMaxAgeDays(config.proofOfAddress));
95
+ case 'proof-of-address': {
96
+ // Word the ask by the rule the server will judge THIS document under:
97
+ // the picked kind, else the first the country offers (what the screen
98
+ // preselects). Under `off` the header stops asking for the name.
99
+ const poa = config.proofOfAddress;
100
+ const kind = poaDocumentType ?? poaDocumentTypes(poa, country)[0];
101
+ return proofOfAddressMeta(
102
+ poaMaxAgeDays(poa),
103
+ poaNamePolicy(poa, country, kind as Parameters<typeof poaNamePolicy>[2]) !== 'off',
104
+ );
105
+ }
106
+ case 'address-search':
107
+ case 'address-collection':
108
+ case 'address-entrance':
109
+ case 'address-review':
110
+ return addressStepMeta(currentStep, config.subjectType === 'business', { framing: addressEntranceFraming });
71
111
  case 'country-select':
72
112
  return countrySelectMeta;
73
113
  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
+ }
@@ -7,6 +7,7 @@
7
7
  // here needs to be authoritative, only honest about what to type.
8
8
  // ---------------------------------------------------------------------------
9
9
 
10
+ import { COMPANY_INFO_FIELDS } from '../types/business';
10
11
  import type {
11
12
  CompanyInfoField,
12
13
  CompanyInfoMode,
@@ -35,6 +36,20 @@ export const BUSINESS_PRODUCTS: readonly BusinessProductDef[] = [
35
36
  inputLabel: 'Registration number',
36
37
  placeholder: 'e.g. RC0000000',
37
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
+ },
38
53
  {
39
54
  key: 'business-tax',
40
55
  label: 'Business + Tax ID',
@@ -90,7 +105,12 @@ export function businessProductsForCountry(
90
105
  ): string[] {
91
106
  const offered = businessProductsFor(business).filter((key) => {
92
107
  const def = BUSINESS_PRODUCTS.find((p) => p.key === key);
93
- 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);
94
114
  });
95
115
  return offered.length > 0 ? offered : [DEFAULT_BUSINESS_PRODUCT];
96
116
  }
@@ -159,12 +179,11 @@ export function companyInfoFieldModes(
159
179
  ): Record<CompanyInfoField, CompanyInfoMode> {
160
180
  const off = business?.collectCompanyInfo === false;
161
181
  const modes = business?.companyInfo ?? {};
162
- return {
163
- address: off ? 'off' : (modes.address ?? 'optional'),
164
- email: off ? 'off' : (modes.email ?? 'optional'),
165
- phone: off ? 'off' : (modes.phone ?? 'optional'),
166
- website: off ? 'off' : (modes.website ?? 'optional'),
167
- };
182
+ // Built from the field list so adding one on the server means adding it in a
183
+ // single place here, rather than a line that is easy to forget.
184
+ return Object.fromEntries(
185
+ COMPANY_INFO_FIELDS.map((f) => [f, off ? 'off' : (modes[f] ?? 'optional')]),
186
+ ) as Record<CompanyInfoField, CompanyInfoMode>;
168
187
  }
169
188
 
170
189
  /** Company-profile fields still missing that the workflow marks required. */
@@ -0,0 +1,52 @@
1
+ import { isValidContactEmail } from './contact';
2
+ import { isValidWebsite } from './website';
3
+ import type { CompanyInfoField, CompanyInfoMode } from '../types/business';
4
+ import type { BusinessCheckState, BusinessState } from '../store/state';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Per-phase validity for the business-details step (200-line rule split).
8
+ //
9
+ // Gated per phase: on the pick screen the detail fields do not exist yet, so
10
+ // holding Continue until they are filled would be waiting on inputs that are
11
+ // not on screen. Empty is fine (optional unless the workflow says otherwise);
12
+ // a malformed value is not. Mirrors the web SDK's isFormValid exactly.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export function businessDetailsValid(input: {
16
+ phase: 'pick' | 'details';
17
+ business: BusinessState;
18
+ modes: Record<CompanyInfoField, CompanyInfoMode>;
19
+ product: string | null | undefined;
20
+ numberValid: boolean;
21
+ nameRequired: boolean;
22
+ showContactEmail: boolean;
23
+ }): boolean {
24
+ const { business, modes } = input;
25
+ const pickValid =
26
+ !!input.product &&
27
+ input.numberValid &&
28
+ (!input.nameRequired || business.registrationName.trim() !== '');
29
+ if (input.phase === 'pick') return pickValid;
30
+
31
+ const showCompanyInfo = Object.values(modes).some((m) => m !== 'off');
32
+ const contactEmailValid =
33
+ business.contactEmail.trim() === '' || isValidContactEmail(business.contactEmail.trim());
34
+ const businessEmailValid =
35
+ business.email.trim() === '' || isValidContactEmail(business.email.trim());
36
+ const companyInfoComplete = (Object.keys(modes) as CompanyInfoField[]).every(
37
+ (f) => modes[f] !== 'required' || business[f].trim() !== '',
38
+ );
39
+ return (
40
+ pickValid &&
41
+ (!input.showContactEmail || contactEmailValid) &&
42
+ (!showCompanyInfo ||
43
+ (businessEmailValid && isValidWebsite(business.website) && companyInfoComplete))
44
+ );
45
+ }
46
+
47
+ /** Whether the check panel has anything to say. Found/skipped/idle stay
48
+ * silent, and so does 'checking' — the loader lives INSIDE the Continue
49
+ * button, since that is the thing the person just pressed. */
50
+ export function checkPanelVisible(status: BusinessCheckState['status']): boolean {
51
+ return status === 'not_found' || status === 'limit_reached' || status === 'unavailable';
52
+ }
@@ -0,0 +1,72 @@
1
+ import type { BusinessCompanyRecord } from '../services/api';
2
+ import type { BusinessState } from '../store/state';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Copy what the register returned into the form, without overwriting anything.
6
+ //
7
+ // Only empty fields are filled: an applicant who typed something before the
8
+ // lookup meant it, and the register is a starting point here rather than the
9
+ // last word. Every value stays editable either way. Returns the patch AND the
10
+ // keys it filled, so a company change can clear exactly those and leave the
11
+ // applicant's own answers alone. Mirrors the web SDK's prefillFromRegister.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export function registerPrefillPatch(
15
+ company: BusinessCompanyRecord | null,
16
+ current: BusinessState,
17
+ ): { patch: Partial<BusinessState>; prefilled: (keyof BusinessState)[] } {
18
+ const patch: Partial<BusinessState> = {};
19
+ if (!company) return { patch, prefilled: [] };
20
+
21
+ // Field by field, from the register's answer to the form's question. The
22
+ // register does not answer all of these for every company, so each is
23
+ // filled only when it actually came back.
24
+ const fill = (
25
+ key:
26
+ | 'registrationName' | 'address' | 'companyType' | 'email' | 'phone'
27
+ | 'taxId' | 'vatNumber' | 'dateOfIncorporation' | 'natureOfBusiness',
28
+ value: string | null | undefined,
29
+ ): void => {
30
+ if (value && !current[key].trim()) patch[key] = value;
31
+ };
32
+ fill('registrationName', company.name);
33
+ // The register splits the address across lines; the form has one box, so
34
+ // they are joined rather than dropping the parts that did not fit.
35
+ fill(
36
+ 'address',
37
+ [company.address, company.city, company.state].filter(Boolean).join(', ') || null,
38
+ );
39
+ fill('companyType', company.typeOfEntity);
40
+ fill('email', company.email);
41
+ fill('phone', company.phone);
42
+ fill('taxId', company.taxId);
43
+ fill('vatNumber', company.vatNumber);
44
+ fill('natureOfBusiness', company.natureOfBusiness);
45
+ // The register gives an incorporation DATE; the field wants YYYY-MM-DD, and
46
+ // anything it cannot be read as is left blank rather than guessed at.
47
+ fill('dateOfIncorporation', isoDateOnly(company.registrationDate));
48
+
49
+ return { patch, prefilled: Object.keys(patch) as (keyof BusinessState)[] };
50
+ }
51
+
52
+ /**
53
+ * The date part of an ISO timestamp, or null.
54
+ *
55
+ * Deliberately NOT `new Date(value)`. That parser is lenient in exactly the
56
+ * wrong direction: it reads "12/03/2018" as 2 December (US order, when a
57
+ * register returning DD/MM means 12 March), "sometime in 2018" as 2017-12-31,
58
+ * and "March 2018" as the 28th. Each of those is a confidently wrong date
59
+ * written into a compliance form, which is worse than an empty field somebody
60
+ * fills in themselves. So only an unambiguous ISO date is accepted, and the
61
+ * calendar is checked afterwards so 2018-02-31 does not roll into March.
62
+ */
63
+ export function isoDateOnly(value: string | null | undefined): string | null {
64
+ if (!value) return null;
65
+ const match = /^(\d{4})-(\d{2})-(\d{2})(?:[T\s]|$)/.exec(value.trim());
66
+ if (!match) return null;
67
+ const [, y, m, d] = match;
68
+ const parsed = new Date(`${y}-${m}-${d}T00:00:00Z`);
69
+ if (Number.isNaN(parsed.getTime())) return null;
70
+ // A real date, not one that rolled over into the next month.
71
+ return parsed.toISOString().slice(0, 10) === `${y}-${m}-${d}` ? `${y}-${m}-${d}` : null;
72
+ }