@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,112 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { MyazaInput } from '../components/MyazaInput';
5
+ import { spacing } from '../config/theme';
6
+ import { CompanyInfoFields } from './CompanyInfoFields';
7
+ import { isValidContactEmail } from '../config/contact';
8
+ import type { BusinessState } from '../store/state';
9
+ import type { BusinessProductDef } from '../config/business';
10
+ import type { RegistrationHint } from '../config/registrationHint';
11
+ import type { CompanyInfoField, CompanyInfoMode } from '../types/business';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // The DETAILS screen of the business step: confirming what the register said.
15
+ //
16
+ // Registration number and name are editable here too — this is also where
17
+ // manual entry lands, for the companies no search index carries. Split from
18
+ // BusinessDetailsStep (200-line rule); field order mirrors the web SDK.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export function BusinessDetailsFields({
22
+ business,
23
+ productDef,
24
+ regHint,
25
+ numberValid,
26
+ formatOk,
27
+ requireName,
28
+ country,
29
+ geoCountry,
30
+ modes,
31
+ showCompanyInfo,
32
+ showContactEmail,
33
+ onChange,
34
+ }: {
35
+ business: BusinessState;
36
+ productDef: BusinessProductDef;
37
+ regHint: RegistrationHint;
38
+ numberValid: boolean;
39
+ formatOk: boolean;
40
+ requireName: boolean;
41
+ country: string;
42
+ geoCountry?: string | null;
43
+ modes: Record<CompanyInfoField, CompanyInfoMode>;
44
+ showCompanyInfo: boolean;
45
+ showContactEmail: boolean;
46
+ onChange: <K extends keyof BusinessState>(key: K, value: BusinessState[K]) => void;
47
+ }): React.ReactElement {
48
+ const regNumber = business.registrationNumber;
49
+ const contactEmail = business.contactEmail;
50
+ const contactEmailInvalid =
51
+ contactEmail.trim() !== '' && !isValidContactEmail(contactEmail.trim());
52
+ return (
53
+ <View>
54
+ <MyazaInput
55
+ label={productDef.inputLabel}
56
+ value={regNumber}
57
+ onChangeText={(text) => onChange('registrationNumber', text)}
58
+ placeholder={regHint.placeholder}
59
+ autoCapitalize="characters"
60
+ autoCorrect={false}
61
+ // Live like the web SDK: a wrong CAC prefix is corrected on the spot,
62
+ // not discovered on Continue. The registry tip fills the same slot
63
+ // until there is an error to show.
64
+ error={
65
+ regNumber !== '' && !numberValid
66
+ ? (!formatOk && regHint.formatError) ||
67
+ `Enter a valid ${productDef.inputLabel.toLowerCase()}.`
68
+ : null
69
+ }
70
+ helper={regHint.tip ?? undefined}
71
+ />
72
+
73
+ <View style={{ height: spacing.md }} />
74
+ <MyazaInput
75
+ label={`Registered business name${requireName ? '' : ' (optional)'}`}
76
+ value={business.registrationName}
77
+ onChangeText={(text) => onChange('registrationName', text)}
78
+ placeholder="Enter the registered business name"
79
+ autoCorrect={false}
80
+ />
81
+
82
+ {showCompanyInfo ? (
83
+ <>
84
+ <View style={{ height: spacing.md }} />
85
+ <CompanyInfoFields
86
+ values={business}
87
+ modes={modes}
88
+ country={country}
89
+ geoCountry={geoCountry}
90
+ onChange={(field, value) => onChange(field, value)}
91
+ />
92
+ </>
93
+ ) : null}
94
+
95
+ {showContactEmail ? (
96
+ <>
97
+ <View style={{ height: spacing.md }} />
98
+ <MyazaInput
99
+ label="Contact email for owner verification (optional)"
100
+ value={contactEmail}
101
+ onChangeText={(text) => onChange('contactEmail', text)}
102
+ placeholder="admin@company.com"
103
+ keyboardType="email-address"
104
+ autoCapitalize="none"
105
+ helper="We'll email this address a link for your directors and owners to verify their identity."
106
+ error={contactEmailInvalid ? 'Enter a valid email address.' : null}
107
+ />
108
+ </>
109
+ ) : null}
110
+ </View>
111
+ );
112
+ }
@@ -2,34 +2,33 @@ import React, { useMemo, useState } from 'react';
2
2
  import { View } from 'react-native';
3
3
 
4
4
  import { spacing } from '../config/theme';
5
- import { useKyc, useKycConfig, useKycStore, useTheme } from '../components/runtime';
6
- import { MyazaText } from '../components/Typography';
5
+ import { useKyc, useKycConfig, useKycStore } from '../components/runtime';
7
6
  import { MyazaButton } from '../components/MyazaButton';
8
- import { MyazaInput } from '../components/MyazaInput';
9
- import { MyazaSelect } from '../components/MyazaSelect';
10
- import { CountryField } from '../components/CountryField';
11
- import { CompanyInfoFields } from './CompanyInfoFields';
7
+ import { BusinessRegistryPickers } from './BusinessRegistryPickers';
8
+ import { BusinessSearch } from './BusinessSearch';
9
+ import { BusinessPickedSection } from './BusinessPickedSection';
10
+ import { BusinessDetailsFields } from './BusinessDetailsFields';
11
+ import { BusinessCheckPanel } from './BusinessCheckPanel';
12
12
  import {
13
13
  businessCountriesFor,
14
- businessCountryName,
15
14
  businessProductsForCountry,
16
15
  companyInfoFieldModes,
17
16
  getBusinessProductDef,
18
17
  keyPeopleNeedsContactEmail,
19
- missingRequiredCompanyInfo,
20
18
  } from '../config/business';
21
19
  import { registrationNumberHint } from '../config/registrationHint';
22
- import { isValidContactEmail } from '../config/contact';
23
- import type { CompanyInfoField } from '../types/business';
20
+ import { registerPrefillPatch } from '../config/businessPrefill';
21
+ import { businessDetailsValid, checkPanelVisible } from '../config/businessDetailsValidity';
24
22
 
25
23
  // ---------------------------------------------------------------------------
26
- // Business (KYB) registry details.
24
+ // Business (KYB) registry details — two screens in one step.
27
25
  //
28
- // The registry lookup verifies that a business EXISTS. Registration numbers are
29
- // public, so it proves nothing about who is filling this in — that is what the
30
- // rest of the KYB application (key people, documents, the applicant's own KYC)
31
- // is for. This step collects the number, the registry country and product, and
32
- // the company profile the workflow asks for.
26
+ // 'pick' is choosing WHICH company. 'details' is confirming what the register
27
+ // then said about it. They are separate because the register has not been
28
+ // asked yet while you are still picking, so showing the detail fields there
29
+ // would invite somebody to fill in answers we are about to overwrite.
30
+ // Continue is what runs the paid check and moves between them. Mirrors the
31
+ // web SDK's BusinessDetailsStep — keep the two in lockstep.
33
32
  // ---------------------------------------------------------------------------
34
33
 
35
34
  export const businessDetailsMeta = {
@@ -41,172 +40,161 @@ export const businessDetailsMeta = {
41
40
  export function BusinessDetailsStep(): React.ReactElement {
42
41
  const config = useKycConfig();
43
42
  const store = useKycStore();
44
- const { colors } = useTheme();
45
43
  const business = useKyc((s) => s.business);
46
- const [touched, setTouched] = useState(false);
44
+ const check = useKyc((s) => s.businessCheck);
45
+ const [phase, setPhase] = useState<'pick' | 'details'>('pick');
46
+ // Production never shows the test-result toggle, and never honours a pin.
47
+ const isSandbox = useKyc((s) => s.serverConfig.environment) !== 'PRODUCTION';
48
+ const geoCountry = useKyc((s) => s.serverConfig.geoCountry);
47
49
 
48
50
  const workflowBusiness = config.business;
49
51
  const countries = useMemo(() => businessCountriesFor(workflowBusiness), [workflowBusiness]);
50
- // Precedence mirrors the web SDK exactly: the visitor's pick, then the
51
- // workflow's PRIMARY country, then the list head. The primary is always in
52
- // the offered list but not necessarily FIRST — preferring `countries[0]`
53
- // defaulted the picker to the wrong registry whenever the primary sat later.
52
+ // Precedence mirrors the web SDK: the visitor's pick, then the workflow's
53
+ // PRIMARY country, then the list head.
54
54
  const country = business.country ?? workflowBusiness?.country ?? countries[0] ?? '';
55
55
  const products = useMemo(
56
56
  () => businessProductsForCountry(workflowBusiness, country),
57
57
  [workflowBusiness, country],
58
58
  );
59
- // Re-derived per country rather than remembered: a product the picked country
60
- // does not offer would be rejected at submit.
61
59
  const product = products.includes(business.product ?? '') ? business.product! : products[0]!;
62
60
  const productDef = getBusinessProductDef(product);
63
61
 
64
62
  const modes = companyInfoFieldModes(workflowBusiness);
65
- const companyFields = (Object.keys(modes) as CompanyInfoField[]).filter(
66
- (field) => modes[field] !== 'off',
67
- );
68
- const needsContactEmail = keyPeopleNeedsContactEmail(workflowBusiness);
63
+ const showCompanyInfo = Object.values(modes).some((m) => m !== 'off');
64
+ const showContactEmail = keyPeopleNeedsContactEmail(workflowBusiness);
69
65
 
70
66
  const set = <K extends keyof typeof business>(key: K, value: (typeof business)[K]): void =>
71
67
  store.getState().setBusinessField(key, value);
72
68
 
73
- const missingCompanyInfo = missingRequiredCompanyInfo(workflowBusiness, {
74
- address: business.address,
75
- email: business.email,
76
- phone: business.phone,
77
- website: business.website,
78
- });
79
- const contactEmailInvalid =
80
- needsContactEmail && business.contactEmail.trim() !== '' && !isValidContactEmail(business.contactEmail);
81
- const nameMissing =
82
- workflowBusiness?.requireRegistrationName === true && business.registrationName.trim() === '';
83
-
84
- // Country-aware registration-number guidance (NG: CAC prefix rules + format
85
- // validation; elsewhere: placeholder + registry tip). Mirrors the web SDK.
86
69
  const regHint = registrationNumberHint(country, productDef);
87
70
  const regNumber = business.registrationNumber;
71
+ // A company has been named, so the card replaces the search. The NUMBER is
72
+ // what names it (a prefilled session sends one without a name; the register
73
+ // supplies the name on Continue, so an unnamed card is momentary).
74
+ const picked = regNumber.trim() !== '';
75
+ const nameRequired = workflowBusiness?.requireRegistrationName === true;
88
76
  const formatOk =
89
77
  !regHint.isValidFormat || regNumber.trim() === '' || regHint.isValidFormat(regNumber);
90
78
  const numberValid = regNumber.trim().length >= 2 && formatOk;
91
- const canContinue =
92
- numberValid && !nameMissing && missingCompanyInfo.length === 0 && !contactEmailInvalid;
93
79
 
94
- const handleContinue = (): void => {
95
- setTouched(true);
96
- if (!canContinue) return;
80
+ const isFormValid = businessDetailsValid({
81
+ phase,
82
+ business,
83
+ modes,
84
+ product,
85
+ numberValid,
86
+ nameRequired,
87
+ showContactEmail,
88
+ });
89
+
90
+ const checking = check.status === 'checking';
91
+
92
+ const handleContinue = async (): Promise<void> => {
93
+ if (!isFormValid || checking) return;
97
94
  // Persist the resolved country/product so the submission uses exactly what
98
- // was on screen, not a re-derivation from a config that may have changed.
99
- set('country', country);
100
- set('product', product);
95
+ // was on screen. Only when different: these are identity keys, and writing
96
+ // an unchanged value would needlessly reset the check we just ran.
97
+ if (business.country !== country) set('country', country);
98
+ if (business.product !== product) set('product', product);
99
+
100
+ // The paid registry check — awaited, so the details screen opens already
101
+ // holding what the register said. Only a definitive "not on the register"
102
+ // stops the flow: everything else (a short balance, an outage) continues
103
+ // and is checked at submission, as it was before.
104
+ const { canContinue, company } = await store.getState().checkBusiness();
105
+ if (!canContinue) return;
106
+
107
+ // First Continue ends at the details screen rather than the next step: the
108
+ // register has only just answered, and this is where what it said gets put
109
+ // in front of them to confirm or correct.
110
+ if (phase === 'pick') {
111
+ const { patch, prefilled } = registerPrefillPatch(company, store.getState().business);
112
+ if (prefilled.length > 0) store.getState().applyBusinessPrefill(patch, prefilled);
113
+ setPhase('details');
114
+ return;
115
+ }
101
116
  store.getState().nextStep();
102
117
  };
103
118
 
104
119
  return (
105
120
  <View>
106
- {countries.length > 1 ? (
107
- <>
108
- <MyazaText variant="bodySmall" style={{ fontWeight: '600', marginBottom: spacing.xs }}>
109
- Country of registration
110
- </MyazaText>
111
- {/* The SAME sheet as the phone field's dial-code picker — restricted
112
- to the workflow's registry countries. */}
113
- <CountryField
114
- value={country}
115
- options={countries.map((code) => ({ code, name: businessCountryName(code) }))}
116
- onChange={(code) => {
117
- set('country', code);
118
- // The product list narrows per country, so a stale pick is
119
- // cleared rather than carried into a refused submission.
120
- set('product', null);
121
- }}
122
- />
123
- <View style={{ height: spacing.md }} />
124
- </>
125
- ) : null}
126
-
127
- {products.length > 1 ? (
128
- <>
129
- <MyazaText variant="bodySmall" style={{ fontWeight: '600', marginBottom: spacing.xs }}>
130
- Verification type
131
- </MyazaText>
132
- <MyazaSelect
133
- value={product}
134
- sheetTitle="Verification type"
135
- options={products.map((key) => ({
136
- value: key,
137
- label: getBusinessProductDef(key).label,
138
- }))}
139
- onChange={(key) => set('product', key)}
140
- />
141
- <View style={{ height: spacing.md }} />
142
- </>
143
- ) : null}
144
-
145
- <MyazaInput
146
- label={productDef.inputLabel}
147
- value={business.registrationNumber}
148
- onChangeText={(text) => set('registrationNumber', text)}
149
- placeholder={regHint.placeholder}
150
- autoCapitalize="characters"
151
- autoFocus
152
- // Live like the web SDK: a wrong CAC prefix is corrected on the spot,
153
- // not discovered on Continue. The registry tip fills the same slot
154
- // until there is an error to show.
155
- error={
156
- regNumber !== '' && !numberValid
157
- ? (!formatOk && regHint.formatError) ||
158
- `Enter a valid ${productDef.inputLabel.toLowerCase()}.`
159
- : null
160
- }
161
- helper={regHint.tip ?? undefined}
121
+ <BusinessRegistryPickers
122
+ country={country}
123
+ countries={countries}
124
+ product={product}
125
+ products={products}
126
+ onCountry={(code) => {
127
+ set('country', code);
128
+ // The product list narrows per country, so a stale pick is cleared
129
+ // rather than carried into a refused submission.
130
+ set('product', null);
131
+ }}
132
+ onProduct={(key) => set('product', key)}
162
133
  />
163
134
 
164
- <View style={{ height: spacing.sm }} />
165
- <MyazaInput
166
- label={`Registered business name${workflowBusiness?.requireRegistrationName ? '' : ' (optional)'}`}
167
- value={business.registrationName}
168
- onChangeText={(text) => set('registrationName', text)}
169
- placeholder="Enter the registered business name"
170
- error={touched && nameMissing ? 'Enter the registered business name.' : null}
171
- />
135
+ {/* HIDDEN, not unmounted, once a company is chosen: unmounting threw
136
+ away the query and results, so "Change" dropped somebody back to an
137
+ empty box. Hiding keeps the picker alive, which makes Change cheap. */}
138
+ <View style={{ display: phase === 'pick' && country && !picked ? 'flex' : 'none' }}>
139
+ <BusinessSearch
140
+ country={country}
141
+ onPicked={(hit) => {
142
+ // Names the company and nothing more: the register is asked on
143
+ // Continue, and the fields it fills live on the screen after.
144
+ set('registrationNumber', hit.registrationNumber);
145
+ set('registrationName', hit.name);
146
+ }}
147
+ onManualEntry={() => setPhase('details')}
148
+ />
149
+ </View>
150
+
151
+ {phase === 'pick' && picked ? (
152
+ <BusinessPickedSection
153
+ country={country}
154
+ name={business.registrationName}
155
+ registrationNumber={regNumber}
156
+ isSandbox={isSandbox}
157
+ onChange={() => {
158
+ // Back to the search rather than an undo: they are changing WHICH
159
+ // company this is about, and the fields belong to the old one.
160
+ set('registrationNumber', '');
161
+ set('registrationName', '');
162
+ }}
163
+ />
164
+ ) : null}
172
165
 
173
- <CompanyInfoFields
174
- fields={companyFields}
175
- modes={modes}
176
- values={{
177
- address: business.address,
178
- email: business.email,
179
- phone: business.phone,
180
- website: business.website,
181
- }}
182
- missing={missingCompanyInfo}
183
- showErrors={touched}
184
- onChange={(field, text) => set(field, text)}
185
- />
166
+ {phase === 'details' ? (
167
+ <BusinessDetailsFields
168
+ business={business}
169
+ productDef={productDef}
170
+ regHint={regHint}
171
+ numberValid={numberValid}
172
+ formatOk={formatOk}
173
+ requireName={nameRequired}
174
+ country={country}
175
+ geoCountry={geoCountry}
176
+ modes={modes}
177
+ showCompanyInfo={showCompanyInfo}
178
+ showContactEmail={showContactEmail}
179
+ onChange={set}
180
+ />
181
+ ) : null}
186
182
 
187
- {needsContactEmail ? (
183
+ {checkPanelVisible(check.status) ? (
188
184
  <>
189
- <View style={{ height: spacing.sm }} />
190
- <MyazaInput
191
- label="Contact email for owner verification (optional)"
192
- value={business.contactEmail}
193
- onChangeText={(text) => set('contactEmail', text)}
194
- placeholder="admin@company.com"
195
- keyboardType="email-address"
196
- autoCapitalize="none"
197
- helper="We'll email this address a link for your directors and owners to verify their identity."
198
- error={touched && contactEmailInvalid ? 'Enter a valid email address.' : null}
199
- />
185
+ <View style={{ height: spacing.lg }} />
186
+ <BusinessCheckPanel check={check} />
200
187
  </>
201
188
  ) : null}
202
189
 
203
- <View style={{ height: spacing.md }} />
204
- <MyazaButton label="Continue" onPress={handleContinue} disabled={touched && !canContinue} />
205
- {touched && !canContinue ? (
206
- <MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.xs, textAlign: 'center' }}>
207
- Please complete the highlighted fields.
208
- </MyazaText>
209
- ) : null}
190
+ <View style={{ height: spacing.lg }} />
191
+ <MyazaButton
192
+ label={checking ? 'Checking…' : phase === 'details' ? 'Confirm details & continue' : 'Continue'}
193
+ onPress={() => void handleContinue()}
194
+ loading={checking}
195
+ disabled={!isFormValid || checking}
196
+ />
210
197
  </View>
211
198
  );
212
199
  }
200
+
@@ -2,15 +2,17 @@ import React from 'react';
2
2
  import { ActivityIndicator, Image, Pressable, View } from 'react-native';
3
3
 
4
4
  import { radius, spacing } from '../config/theme';
5
+ import { UPLOAD_HINT } from '../config/uploadLimits';
5
6
  import { useTheme } from '../components/runtime';
6
7
  import { MyazaText } from '../components/Typography';
7
8
  import { Icon } from '../components/Icon';
9
+ import { DashedBorder } from '../components/DashedBorder';
8
10
 
9
11
  // ---------------------------------------------------------------------------
10
12
  // One business-document upload slot — mirrors the web SDK's
11
13
  // BusinessDocumentSlot 1:1: empty, it is a dashed full-width tap target
12
14
  // (upload glyph — a spinner while uploading — label with a red * when
13
- // required, "Photo or PDF, up to 20MB"); filled, a solid card showing a 48px
15
+ // required, the shared upload hint); filled, a solid card showing a 48px
14
16
  // THUMBNAIL of what was actually picked (a muted file tile for PDFs, a check
15
17
  // when the preview didn't survive a remount — web's exact fallback ladder),
16
18
  // the file's name over the slot label, Replace, and remove. Showing the file
@@ -134,15 +136,13 @@ export function BusinessDocumentSlot({
134
136
  style={{
135
137
  flexDirection: 'row',
136
138
  alignItems: 'center',
137
- borderWidth: 2,
138
- borderStyle: 'dashed',
139
- borderColor: colors.border,
140
139
  borderRadius: radius.sm,
141
140
  padding: spacing.md,
142
141
  marginBottom: spacing.sm + 4,
143
142
  opacity: uploading ? 0.7 : 1,
144
143
  }}
145
144
  >
145
+ <DashedBorder color={colors.border} radius={radius.sm} />
146
146
  {uploading ? (
147
147
  <ActivityIndicator size="small" color={colors.primary} />
148
148
  ) : (
@@ -159,7 +159,7 @@ export function BusinessDocumentSlot({
159
159
  ) : null}
160
160
  </MyazaText>
161
161
  <MyazaText variant="bodySmall" color={colors.textMuted}>
162
- {uploading ? 'Uploading…' : 'Photo or PDF, up to 20MB'}
162
+ {uploading ? 'Uploading…' : UPLOAD_HINT}
163
163
  </MyazaText>
164
164
  </View>
165
165
  </Pressable>
@@ -1,6 +1,5 @@
1
1
  import React, { useCallback, useMemo, useState } from 'react';
2
2
  import { View } from 'react-native';
3
- import * as ImagePicker from 'expo-image-picker';
4
3
 
5
4
  import { spacing } from '../config/theme';
6
5
  import { useKyc, useKycConfig, useKycStore, useTheme } from '../components/runtime';
@@ -11,8 +10,7 @@ import { BusinessDocumentSlot } from './BusinessDocumentSlot';
11
10
  import { withRetry } from '../services/retry';
12
11
  import { compressDocumentImage } from '../services/mediaCompress';
13
12
  import { resolveBusinessDocumentTypes, type ResolvedBusinessDocumentType } from '../config/businessSteps';
14
- import { isAcceptedPoaMimeType, POA_ACCEPTED_MIME_TYPES } from '../config/proofOfAddress';
15
- import { loadDocumentPicker } from '../services/documentPicker';
13
+ import { useBusinessDocumentAttach } from './useBusinessDocumentAttach';
16
14
 
17
15
  // ---------------------------------------------------------------------------
18
16
  // Supporting company documents (certificate of incorporation, MEMART, …).
@@ -26,13 +24,14 @@ import { loadDocumentPicker } from '../services/documentPicker';
26
24
  // Layout mirrors the web SDK 1:1: the header carries the description, each
27
25
  // document is a dashed upload slot (see BusinessDocumentSlot), and Continue is
28
26
  // disabled until every required slot is filled. Tapping a slot opens the
29
- // photo/file source sheet — the mobile stand-in for web's file input.
27
+ // photo/file source sheet — the mobile stand-in for web's file input; the
28
+ // pickers themselves live in useBusinessDocumentAttach.
30
29
  // ---------------------------------------------------------------------------
31
30
 
32
31
  export const businessDocumentsMeta = {
33
32
  title: 'Business documents',
34
33
  description:
35
- 'Upload the supporting documents for your business. Required documents are marked with *.',
34
+ 'Upload the supporting documents for your business. Each one must clearly show the registered business name and registration number. Required documents are marked with *.',
36
35
  };
37
36
 
38
37
  export function BusinessDocumentsStep(): React.ReactElement {
@@ -79,57 +78,7 @@ export function BusinessDocumentsStep(): React.ReactElement {
79
78
  [store],
80
79
  );
81
80
 
82
- const takePhoto = useCallback(
83
- async (slot: ResolvedBusinessDocumentType) => {
84
- const permission = await ImagePicker.requestCameraPermissionsAsync();
85
- if (!permission.granted) {
86
- setError('Camera access is needed to photograph the document.');
87
- return;
88
- }
89
- const result = await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], quality: 1 });
90
- const asset = result.canceled ? undefined : result.assets[0];
91
- if (asset) {
92
- // A FRIENDLY name, not the picker's temp junk — camera/library assets
93
- // carry generated names; the slot key says what the file IS.
94
- await attach(slot, asset.uri, asset.mimeType ?? 'image/jpeg', `${slot.key}.jpg`);
95
- }
96
- },
97
- [attach],
98
- );
99
-
100
- const choosePhoto = useCallback(
101
- async (slot: ResolvedBusinessDocumentType) => {
102
- const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], quality: 1 });
103
- const asset = result.canceled ? undefined : result.assets[0];
104
- if (asset) {
105
- await attach(slot, asset.uri, asset.mimeType ?? 'image/jpeg', `${slot.key}.jpg`);
106
- }
107
- },
108
- [attach],
109
- );
110
-
111
- const chooseFile = useCallback(
112
- async (slot: ResolvedBusinessDocumentType) => {
113
- const picker = loadDocumentPicker();
114
- if (!picker) {
115
- setError('Choosing a file is not available in this app. Please photograph the document.');
116
- return;
117
- }
118
- const result = await picker.getDocumentAsync({
119
- type: [...POA_ACCEPTED_MIME_TYPES],
120
- copyToCacheDirectory: true,
121
- multiple: false,
122
- });
123
- const asset = result.canceled ? undefined : result.assets?.[0];
124
- if (!asset) return;
125
- if (!isAcceptedPoaMimeType(asset.mimeType)) {
126
- setError('Please choose a photo (JPEG, PNG or WebP) or a PDF.');
127
- return;
128
- }
129
- await attach(slot, asset.uri, asset.mimeType, asset.name ?? slot.key);
130
- },
131
- [attach],
132
- );
81
+ const { takePhoto, choosePhoto, chooseFile } = useBusinessDocumentAttach(attach, setError);
133
82
 
134
83
  const handleContinue = (): void => {
135
84
  if (missing.length > 0 || busySlot !== null) return;