@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,155 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../config/theme';
5
+ import { useTheme } from './runtime';
6
+ import { Icon, type IconName } from './Icon';
7
+ import { MyazaText } from './Typography';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // The presence story's three-node track, shared by the intro gate (the promise)
11
+ // and the success card (the same promise, now running) so the two read as two
12
+ // frames of ONE story rather than two unrelated notices.
13
+ //
14
+ // Nodes ahead of the active one are TINTED, not greyed out: they are the plan,
15
+ // not disabled controls. Flat fills only.
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export interface Milestone {
19
+ icon: IconName;
20
+ /** The when: "Your part", "Today", "Then". */
21
+ stage: string;
22
+ title: string;
23
+ caption: string;
24
+ state: 'active' | 'ahead';
25
+ }
26
+
27
+ export function MilestoneTrack({
28
+ milestones,
29
+ numbered = false,
30
+ nodeSize = 40,
31
+ }: {
32
+ milestones: readonly Milestone[];
33
+ /** Wear the step number on each node — the sequence is the point on the
34
+ * intro screen, where nothing has happened yet. */
35
+ numbered?: boolean;
36
+ /** Node diameter, and therefore where the connecting rail has to sit: 40 on
37
+ * the primer (web h-10), 32 on the success card (web h-8). */
38
+ nodeSize?: number;
39
+ }): React.ReactElement {
40
+ const { colors } = useTheme();
41
+ const NODE = nodeSize;
42
+ // The active node's halo: web's `shadow-[0_0_0_5px] shadow-primary/15`
43
+ // (4px on the smaller success-card node), drawn as a ring behind it.
44
+ const halo = NODE >= 40 ? 5 : 4;
45
+ return (
46
+ <View style={{ paddingHorizontal: spacing.md, paddingVertical: spacing.md }}>
47
+ {milestones.map((m, i) => {
48
+ const active = m.state === 'active';
49
+ const last = i === milestones.length - 1;
50
+ return (
51
+ <View
52
+ key={m.title}
53
+ style={{ flexDirection: 'row', gap: spacing.md, paddingBottom: last ? 0 : spacing.md }}
54
+ >
55
+ {!last ? (
56
+ <View
57
+ pointerEvents="none"
58
+ style={{
59
+ position: 'absolute',
60
+ left: NODE / 2 - 0.5,
61
+ top: NODE + 4,
62
+ bottom: 0,
63
+ width: 1,
64
+ backgroundColor: `${colors.primary}33`,
65
+ }}
66
+ />
67
+ ) : null}
68
+
69
+ <View style={{ width: NODE }}>
70
+ {active ? (
71
+ <View
72
+ pointerEvents="none"
73
+ style={{
74
+ position: 'absolute',
75
+ left: -halo,
76
+ top: -halo,
77
+ width: NODE + halo * 2,
78
+ height: NODE + halo * 2,
79
+ borderRadius: radius.full,
80
+ backgroundColor: `${colors.primary}26`,
81
+ }}
82
+ />
83
+ ) : null}
84
+ <View
85
+ style={{
86
+ width: NODE,
87
+ height: NODE,
88
+ borderRadius: radius.full,
89
+ alignItems: 'center',
90
+ justifyContent: 'center',
91
+ backgroundColor: active ? colors.primary : `${colors.primary}1A`,
92
+ ...(active ? {} : { borderWidth: 1, borderColor: `${colors.primary}33` }),
93
+ }}
94
+ >
95
+ <Icon name={m.icon} size={NODE * 0.45} color={active ? colors.onPrimary : colors.primary} />
96
+ </View>
97
+ {numbered ? (
98
+ <View
99
+ style={{
100
+ position: 'absolute',
101
+ right: -4,
102
+ top: -4,
103
+ width: 18,
104
+ height: 18,
105
+ borderRadius: radius.full,
106
+ borderWidth: 1,
107
+ borderColor: active ? colors.primary : colors.border,
108
+ backgroundColor: colors.background,
109
+ alignItems: 'center',
110
+ justifyContent: 'center',
111
+ }}
112
+ >
113
+ <MyazaText
114
+ variant="bodySmall"
115
+ color={active ? colors.primary : colors.textSecondary}
116
+ style={{ fontSize: 9, fontWeight: '700', lineHeight: 11 }}
117
+ >
118
+ {i + 1}
119
+ </MyazaText>
120
+ </View>
121
+ ) : null}
122
+ </View>
123
+
124
+ <View style={{ flex: 1, paddingTop: 2 }}>
125
+ {/* Uppercased by STYLE, not by the string: a screen reader
126
+ should hear the words, not shouted initials. */}
127
+ <MyazaText
128
+ variant="bodySmall"
129
+ color={active ? colors.primary : colors.textSecondary}
130
+ style={{
131
+ fontSize: 10,
132
+ fontWeight: '600',
133
+ letterSpacing: 0.8,
134
+ textTransform: 'uppercase',
135
+ }}
136
+ >
137
+ {m.stage}
138
+ </MyazaText>
139
+ <MyazaText variant="bodyMedium" style={{ fontWeight: '600', marginTop: 1 }}>
140
+ {m.title}
141
+ </MyazaText>
142
+ <MyazaText
143
+ variant="bodySmall"
144
+ color={colors.textSecondary}
145
+ style={{ marginTop: 2 }}
146
+ >
147
+ {m.caption}
148
+ </MyazaText>
149
+ </View>
150
+ </View>
151
+ );
152
+ })}
153
+ </View>
154
+ );
155
+ }
@@ -0,0 +1,107 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../config/theme';
5
+ import { resolveIdTypeDefinition } from '../config/idTypes';
6
+ import type { MultiIdPlan } from '../lib/multi-id';
7
+ import type { MultiIdSlot } from '../store/state';
8
+ import { useEffectiveCountry, useKyc, useTheme } from './runtime';
9
+ import { MyazaText } from './Typography';
10
+ import { Icon } from './Icon';
11
+
12
+ // The multi-ID run's position strip: one chip per check, filling in with the
13
+ // picked ID's name as each check commits, the current one highlighted.
14
+ //
15
+ // A PORT of the web SDK's MultiIdProgress. Rendered above the picker, the
16
+ // evidence steps and liveness so a reader always knows which of the run's IDs
17
+ // the screen is about and how many remain — without it a three-ID run is three
18
+ // visits to the same-looking screen with nothing saying which is which.
19
+
20
+ export function MultiIdProgress({ plan }: { plan: MultiIdPlan }): React.ReactElement {
21
+ const { colors } = useTheme();
22
+ const country = useEffectiveCountry();
23
+ const serverConfig = useKyc((s) => s.serverConfig);
24
+ const slots: MultiIdSlot[] = useKyc((s) => s.multiIdSlots);
25
+
26
+ const labelFor = (idType: string): string => {
27
+ const row = serverConfig.idTypes.find(
28
+ (r) => r.country === country && r.idType === idType,
29
+ );
30
+ return resolveIdTypeDefinition(country, idType, {
31
+ label: row?.label,
32
+ requiresDocumentCapture: row?.requiresDocumentCapture,
33
+ scanSides: row?.scanSides,
34
+ supportsNfc: row?.supportsNfc,
35
+ }).label;
36
+ };
37
+
38
+ return (
39
+ <View
40
+ accessibilityLabel={`ID ${Math.min(plan.index + 1, plan.count)} of ${plan.count}`}
41
+ style={{
42
+ flexDirection: 'row',
43
+ alignItems: 'center',
44
+ gap: spacing.sm,
45
+ marginBottom: spacing.md,
46
+ paddingHorizontal: spacing.md,
47
+ paddingVertical: spacing.sm,
48
+ borderRadius: radius.md,
49
+ borderWidth: 1,
50
+ borderColor: colors.border,
51
+ backgroundColor: colors.backgroundSecondary,
52
+ }}
53
+ >
54
+ {Array.from({ length: plan.count }, (_, i) => {
55
+ const committed = slots[i];
56
+ const active = i === plan.index;
57
+ return (
58
+ <View
59
+ key={i}
60
+ style={{ flexDirection: 'row', alignItems: 'center', gap: spacing.sm, minWidth: 0 }}
61
+ >
62
+ {i > 0 && (
63
+ <View
64
+ style={{
65
+ height: 1,
66
+ width: 16,
67
+ backgroundColor: committed || active ? colors.primary : colors.border,
68
+ opacity: committed || active ? 0.5 : 1,
69
+ }}
70
+ />
71
+ )}
72
+ <View
73
+ style={{
74
+ height: 20,
75
+ width: 20,
76
+ borderRadius: 10,
77
+ alignItems: 'center',
78
+ justifyContent: 'center',
79
+ backgroundColor: committed ? colors.primary : 'transparent',
80
+ borderWidth: committed ? 0 : active ? 2 : 1,
81
+ borderColor: active ? colors.primary : colors.border,
82
+ }}
83
+ >
84
+ {committed ? (
85
+ <Icon name="check" size={12} color={colors.onPrimary} />
86
+ ) : (
87
+ <MyazaText
88
+ variant="bodySmall"
89
+ color={active ? colors.primary : colors.textMuted}
90
+ >
91
+ {String(i + 1)}
92
+ </MyazaText>
93
+ )}
94
+ </View>
95
+ <MyazaText
96
+ variant="bodySmall"
97
+ color={active ? colors.textDark : colors.textMuted}
98
+ numberOfLines={1}
99
+ >
100
+ {committed ? labelFor(committed.idType) : `ID ${i + 1}`}
101
+ </MyazaText>
102
+ </View>
103
+ );
104
+ })}
105
+ </View>
106
+ );
107
+ }
@@ -21,6 +21,9 @@ export interface MyazaInputProps {
21
21
  value: string;
22
22
  onChangeText: (text: string) => void;
23
23
  label?: string;
24
+ /** Marks the label with an asterisk in the error colour: a field the flow
25
+ * will not continue without. */
26
+ required?: boolean;
24
27
  placeholder?: string;
25
28
  error?: string | null;
26
29
  helper?: string;
@@ -39,6 +42,21 @@ export interface MyazaInputProps {
39
42
  /** Rendered inside the field, after the text — a unit like "%". Same
40
43
  * bordered-row treatment as `prefix`. */
41
44
  suffix?: React.ReactNode;
45
+ /** Field height override — the results filter runs at 40 where every other
46
+ * input is 48, matching the web SDK's h-10 filter beside h-12 inputs. */
47
+ height?: number;
48
+ /** Text size override, for the compact filter (web: text-sm = 14). */
49
+ fontSize?: number;
50
+ /**
51
+ * Off for proper-noun fields (company names, registration numbers): iOS
52
+ * autocorrect rewrites them right before the person submits, so a search
53
+ * for the company they typed silently becomes a search for a word the
54
+ * keyboard preferred. Defaults on, like the platform.
55
+ */
56
+ autoCorrect?: boolean;
57
+ /** Multi-line entry (directions, notes): three visible lines, text anchored
58
+ * to the top on Android (which centres multiline text by default). */
59
+ multiline?: boolean;
42
60
  onFocus?: FocusHandler;
43
61
  onBlur?: BlurHandler;
44
62
  }
@@ -47,6 +65,7 @@ export function MyazaInput({
47
65
  value,
48
66
  onChangeText,
49
67
  label,
68
+ required = false,
50
69
  placeholder,
51
70
  error,
52
71
  helper,
@@ -54,9 +73,13 @@ export function MyazaInput({
54
73
  autoCapitalize = 'none',
55
74
  maxLength,
56
75
  editable = true,
76
+ multiline = false,
57
77
  autoFocus = false,
58
78
  prefix,
59
79
  suffix,
80
+ height,
81
+ fontSize,
82
+ autoCorrect = true,
60
83
  onFocus,
61
84
  onBlur,
62
85
  }: MyazaInputProps): React.ReactElement {
@@ -86,6 +109,11 @@ export function MyazaInput({
86
109
  {label ? (
87
110
  <MyazaText variant="label" style={{ marginBottom: spacing.sm }}>
88
111
  {label}
112
+ {required ? (
113
+ <MyazaText variant="label" color={colors.error}>
114
+ {' *'}
115
+ </MyazaText>
116
+ ) : null}
89
117
  </MyazaText>
90
118
  ) : null}
91
119
  {prefix || suffix ? (
@@ -95,7 +123,7 @@ export function MyazaInput({
95
123
  style={{
96
124
  flexDirection: 'row',
97
125
  alignItems: 'center',
98
- height: sizing.inputHeight,
126
+ height: height ?? sizing.inputHeight,
99
127
  borderWidth,
100
128
  borderColor,
101
129
  borderRadius: radius.sm,
@@ -112,12 +140,14 @@ export function MyazaInput({
112
140
  placeholderTextColor={colors.textMuted}
113
141
  keyboardType={keyboardType}
114
142
  autoCapitalize={autoCapitalize}
143
+ autoCorrect={autoCorrect}
144
+ spellCheck={autoCorrect}
115
145
  maxLength={maxLength}
116
146
  editable={editable}
117
147
  autoFocus={autoFocus}
118
148
  onFocus={handleFocus}
119
149
  onBlur={handleBlur}
120
- style={{ flex: 1, height: '100%', color: colors.textDark, fontSize: 16, fontFamily }}
150
+ style={{ flex: 1, height: '100%', color: colors.textDark, fontSize: fontSize ?? 16, fontFamily }}
121
151
  />
122
152
  {suffix ? <View style={{ width: spacing.sm }} /> : null}
123
153
  {suffix}
@@ -130,13 +160,18 @@ export function MyazaInput({
130
160
  placeholderTextColor={colors.textMuted}
131
161
  keyboardType={keyboardType}
132
162
  autoCapitalize={autoCapitalize}
163
+ autoCorrect={autoCorrect}
164
+ spellCheck={autoCorrect}
133
165
  maxLength={maxLength}
134
166
  editable={editable}
135
167
  autoFocus={autoFocus}
168
+ multiline={multiline}
169
+ numberOfLines={multiline ? 3 : 1}
170
+ textAlignVertical={multiline ? 'top' : 'center'}
136
171
  onFocus={handleFocus}
137
172
  onBlur={handleBlur}
138
173
  style={{
139
- height: sizing.inputHeight,
174
+ height: height ?? (multiline ? sizing.inputHeight * 2 : sizing.inputHeight),
140
175
  borderWidth,
141
176
  borderColor,
142
177
  borderRadius: radius.sm,
@@ -145,7 +180,7 @@ export function MyazaInput({
145
180
  paddingHorizontal: spacing.md + (2 - borderWidth),
146
181
  color: colors.textDark,
147
182
  backgroundColor: colors.background,
148
- fontSize: 16,
183
+ fontSize: fontSize ?? 16,
149
184
  fontFamily,
150
185
  }}
151
186
  />
@@ -147,7 +147,8 @@ export function MyazaSelect<T extends string | number>({
147
147
  style={{
148
148
  fontWeight: '700',
149
149
  paddingHorizontal: spacing.md,
150
- paddingTop: spacing.md,
150
+ // Tight under the handle, the way the system sheet titles sit.
151
+ paddingTop: spacing.xs,
151
152
  paddingBottom: spacing.sm,
152
153
  }}
153
154
  >
@@ -22,27 +22,40 @@ export function OptionRow({
22
22
  label,
23
23
  selected,
24
24
  multi,
25
+ role,
26
+ icon,
25
27
  leading,
26
28
  trailing,
29
+ disabled,
27
30
  onPress,
28
31
  }: {
29
32
  label: string;
30
33
  selected: boolean;
31
34
  /** Renders a square check instead of a radio dot. */
32
35
  multi?: boolean;
36
+ /**
37
+ * What the row IS to a screen reader. Defaults from `multi`; a single-pick
38
+ * list wearing the square check (the PoA document kind) passes 'radio'.
39
+ */
40
+ role?: 'checkbox' | 'radio';
41
+ /** A glyph between the mark and the label — the mark stays. */
42
+ icon?: React.ReactNode;
33
43
  /** Something before the label — a flag, an icon. Replaces the radio mark. */
34
44
  leading?: React.ReactNode;
35
45
  trailing?: React.ReactNode;
46
+ /** Greyed and inert — a choice that is locked, not one that is gone. */
47
+ disabled?: boolean;
36
48
  onPress: () => void;
37
49
  }): React.ReactElement {
38
50
  const { colors } = useTheme();
39
51
  return (
40
52
  <Pressable
41
- onPress={onPress}
42
- accessibilityRole={multi ? 'checkbox' : 'radio'}
43
- accessibilityState={{ checked: selected }}
53
+ onPress={disabled ? undefined : onPress}
54
+ accessibilityRole={role ?? (multi ? 'checkbox' : 'radio')}
55
+ accessibilityState={{ checked: selected, disabled: disabled === true }}
44
56
  accessibilityLabel={label}
45
57
  style={{
58
+ opacity: disabled ? 0.5 : 1,
46
59
  flexDirection: 'row',
47
60
  alignItems: 'center',
48
61
  // The checkbox variant matches the web SDK's multi-select card exactly
@@ -59,6 +72,12 @@ export function OptionRow({
59
72
  >
60
73
  {leading ?? <SelectionMark selected={selected} multi={multi} />}
61
74
  <View style={{ width: multi ? 10 : spacing.sm }} />
75
+ {icon ? (
76
+ <>
77
+ {icon}
78
+ <View style={{ width: 10 }} />
79
+ </>
80
+ ) : null}
62
81
  <MyazaText variant="body" style={{ flex: 1 }}>
63
82
  {label}
64
83
  </MyazaText>
@@ -0,0 +1,102 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import { PanResponder, View } from 'react-native';
3
+
4
+ import { useTheme } from './runtime';
5
+
6
+ // ─── A 0–100 slider for the ownership field ──────────────────────────────────
7
+ //
8
+ // The fast coarse gesture beside the exact box: dragging writes whole numbers
9
+ // into the same field, and a register stake like 48.42 is still typed (no
10
+ // thumb lands on it). Dependency-free — a track, a fill and a thumb under one
11
+ // PanResponder — because a native slider module is a heavy import for one
12
+ // field. Mirrors the web SDK's range input under the same box.
13
+
14
+ const THUMB = 22;
15
+ const TRACK = 5;
16
+
17
+ export function OwnershipSlider({
18
+ value,
19
+ onChange,
20
+ }: {
21
+ /** The current stake 0–100; an undeclared one rests the thumb at 0. */
22
+ value: number;
23
+ onChange: (next: number) => void;
24
+ }): React.ReactElement {
25
+ const { colors } = useTheme();
26
+ const [width, setWidth] = useState(0);
27
+ const widthRef = useRef(0);
28
+ const onChangeRef = useRef(onChange);
29
+ onChangeRef.current = onChange;
30
+
31
+ const setFromX = (x: number): void => {
32
+ const usable = widthRef.current - THUMB;
33
+ if (usable <= 0) return;
34
+ const ratio = (x - THUMB / 2) / usable;
35
+ const next = Math.round(Math.min(1, Math.max(0, ratio)) * 100);
36
+ onChangeRef.current(next);
37
+ };
38
+
39
+ const pan = useRef(
40
+ PanResponder.create({
41
+ onStartShouldSetPanResponder: () => true,
42
+ onMoveShouldSetPanResponder: () => true,
43
+ onPanResponderGrant: (e) => setFromX(e.nativeEvent.locationX),
44
+ onPanResponderMove: (e) => setFromX(e.nativeEvent.locationX),
45
+ }),
46
+ ).current;
47
+
48
+ const clamped = Math.min(100, Math.max(0, value));
49
+ const left = width > 0 ? (clamped / 100) * (width - THUMB) : 0;
50
+
51
+ return (
52
+ <View
53
+ {...pan.panHandlers}
54
+ onLayout={(e) => {
55
+ widthRef.current = e.nativeEvent.layout.width;
56
+ setWidth(e.nativeEvent.layout.width);
57
+ }}
58
+ accessibilityRole="adjustable"
59
+ accessibilityLabel="Ownership percentage"
60
+ accessibilityValue={{ min: 0, max: 100, now: clamped }}
61
+ // Tall hit area; the visible track stays thin.
62
+ style={{ height: 32, justifyContent: 'center' }}
63
+ >
64
+ <View
65
+ style={{
66
+ height: TRACK,
67
+ borderRadius: TRACK / 2,
68
+ backgroundColor: colors.border,
69
+ }}
70
+ />
71
+ <View
72
+ pointerEvents="none"
73
+ style={{
74
+ position: 'absolute',
75
+ left: 0,
76
+ width: left + THUMB / 2,
77
+ height: TRACK,
78
+ borderRadius: TRACK / 2,
79
+ backgroundColor: colors.primary,
80
+ }}
81
+ />
82
+ <View
83
+ pointerEvents="none"
84
+ style={{
85
+ position: 'absolute',
86
+ left,
87
+ width: THUMB,
88
+ height: THUMB,
89
+ borderRadius: THUMB / 2,
90
+ backgroundColor: colors.primary,
91
+ borderWidth: 3,
92
+ borderColor: colors.background,
93
+ shadowColor: '#000',
94
+ shadowOpacity: 0.15,
95
+ shadowRadius: 3,
96
+ shadowOffset: { width: 0, height: 1 },
97
+ elevation: 2,
98
+ }}
99
+ />
100
+ </View>
101
+ );
102
+ }
@@ -1,4 +1,4 @@
1
- import React, { useMemo, useState } from 'react';
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { Pressable, View } from 'react-native';
3
3
 
4
4
  import {
@@ -34,13 +34,32 @@ import { COUNTRY_NAMES } from '../config/countryNames.g';
34
34
  // ---------------------------------------------------------------------------
35
35
 
36
36
  export function PhoneNumberInput({
37
+ value,
37
38
  defaultCountry,
39
+ geoCountry,
38
40
  disabled,
41
+ autoFocus = false,
39
42
  onChange,
40
43
  }: {
44
+ /**
45
+ * E.164 seed. The field itself stays uncontrolled (the formatter owns the
46
+ * text), but a value arriving after mount — the register's, prefilled a
47
+ * moment later — has to reach the field, or we hold a number the applicant
48
+ * can neither see nor correct and submit it as though they gave it.
49
+ */
50
+ value?: string;
41
51
  /** Seeds the picker (ISO-2). Falls back to NG. */
42
52
  defaultCountry?: string;
53
+ /**
54
+ * The visitor's IP country. Pinned to the top of the picker and tagged, so
55
+ * a guess we made on their behalf is visible AS a guess and one tap away
56
+ * rather than buried at its alphabetical position. Mirrors the web SDK.
57
+ */
58
+ geoCountry?: string | null;
43
59
  disabled?: boolean;
60
+ /** Focus + keyboard on mount. Opt-in: right for a single-field OTP screen,
61
+ * wrong for a row in a company-profile form. */
62
+ autoFocus?: boolean;
44
63
  /** Fires on every edit with the E.164 value ('' until parseable) + validity. */
45
64
  onChange: (value: { e164: string; isValid: boolean; country: string }) => void;
46
65
  }): React.ReactElement {
@@ -64,11 +83,32 @@ export function PhoneNumberInput({
64
83
  [],
65
84
  );
66
85
 
67
- const seed = (defaultCountry?.toUpperCase() ?? 'NG') as CountryCode;
86
+ // A supplied number decides its OWN dial code — it is a fact, where the
87
+ // country prop is a guess. Falls through to the guess when there is no
88
+ // number or it cannot be parsed.
89
+ const supplied = value ? parsePhoneNumberFromString(value) : null;
90
+ const seed = (supplied?.country ??
91
+ defaultCountry?.toUpperCase() ??
92
+ 'NG') as CountryCode;
68
93
  const [country, setCountry] = useState<CountryCode>(
69
94
  options.some((o) => o.code === seed) ? seed : ('NG' as CountryCode),
70
95
  );
71
- const [national, setNational] = useState('');
96
+ const [national, setNational] = useState(() =>
97
+ supplied ? formatNationalNumber(supplied.nationalNumber, seed) : '',
98
+ );
99
+ // A number that arrives AFTER mount — the register's, prefilled a moment
100
+ // later — has to reach the field. Seeding is one-way and keyed on the value,
101
+ // so the applicant's own edits are never fought.
102
+ const seededRef = useRef(value ?? '');
103
+ useEffect(() => {
104
+ if (!value || value === seededRef.current) return;
105
+ seededRef.current = value;
106
+ const parsed = parsePhoneNumberFromString(value);
107
+ if (!parsed) return;
108
+ const next = (parsed.country ?? country) as CountryCode;
109
+ setCountry(next);
110
+ setNational(formatNationalNumber(parsed.nationalNumber, next));
111
+ }, [value, country]);
72
112
  const [open, setOpen] = useState(false);
73
113
 
74
114
  const selected = options.find((o) => o.code === country);
@@ -132,7 +172,7 @@ export function PhoneNumberInput({
132
172
  placeholder="803 123 4567"
133
173
  keyboardType="phone-pad"
134
174
  editable={!disabled}
135
- autoFocus
175
+ autoFocus={autoFocus}
136
176
  />
137
177
  </View>
138
178
  </View>
@@ -141,6 +181,7 @@ export function PhoneNumberInput({
141
181
  visible={open}
142
182
  options={options}
143
183
  selected={country}
184
+ pinned={geoCountry}
144
185
  onPick={pickCountry}
145
186
  onClose={() => setOpen(false)}
146
187
  />
@@ -36,12 +36,12 @@ export function PoweredBy({ bottomInset = 0 }: { bottomInset?: number }): React.
36
36
  return (
37
37
  <View
38
38
  style={{
39
- paddingTop: spacing.md,
39
+ paddingTop: 10,
40
40
  // Symmetric with the top. It was `lg`, which read as a heavy band on the
41
41
  // one screen where vertical space is contested — the searchable country
42
42
  // list, where the footer costs a visible row. The home-indicator inset
43
43
  // rides on top, so notched devices still clear it comfortably.
44
- paddingBottom: spacing.md + bottomInset,
44
+ paddingBottom: 12 + bottomInset,
45
45
  alignItems: 'center',
46
46
  }}
47
47
  >
@@ -70,9 +70,9 @@ export function PoweredBy({ bottomInset = 0 }: { bottomInset?: number }): React.
70
70
  hitSlop={8}
71
71
  style={{ flexDirection: 'row', alignItems: 'center', gap: spacing.sm }}
72
72
  >
73
- <MyazaWordmark height={28} wordmark={markColor} />
73
+ <MyazaWordmark height={24} wordmark={markColor} />
74
74
 
75
- <View style={{ width: 1, height: 24, backgroundColor: markColor, opacity: 0.35 }} />
75
+ <View style={{ width: 1, height: 20, backgroundColor: markColor, opacity: 0.35 }} />
76
76
 
77
77
  {/* ~half the wordmark's height — the ratio the dashboard lockup uses. */}
78
78
  {/* Pinned to the brand face rather than inheriting: this word is part
@@ -83,7 +83,7 @@ export function PoweredBy({ bottomInset = 0 }: { bottomInset?: number }): React.
83
83
  brandMark
84
84
  variant="body"
85
85
  color={markColor}
86
- style={{ flexShrink: 1, fontSize: 14, fontWeight: '600', letterSpacing: 2 }}
86
+ style={{ flexShrink: 1, fontSize: 12, fontWeight: '600', letterSpacing: 1.68 }}
87
87
  >
88
88
  TRUST
89
89
  </MyazaText>