@myazahq/kyc-sdk-react-native 2.3.0 → 2.5.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 (129) hide show
  1. package/android/build.gradle +35 -3
  2. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +75 -0
  3. package/ios/HybridMyazaFaceDetector.swift +8 -0
  4. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
  5. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
  6. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
  7. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
  8. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
  9. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
  10. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  11. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
  12. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
  13. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
  14. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
  15. package/package.json +1 -2
  16. package/src/MyazaKYC.tsx +12 -0
  17. package/src/components/BrandBar.tsx +137 -0
  18. package/src/components/DashedBorder.tsx +55 -0
  19. package/src/components/DialCodePicker.tsx +25 -24
  20. package/src/components/DocumentReviewSide.tsx +34 -14
  21. package/src/components/Icon.tsx +14 -0
  22. package/src/components/KycSheet.tsx +39 -140
  23. package/src/components/MediaSourceSheet.tsx +7 -37
  24. package/src/components/MultiIdProgress.tsx +107 -0
  25. package/src/components/MyazaDateField.tsx +6 -20
  26. package/src/components/MyazaInput.tsx +23 -4
  27. package/src/components/MyazaSelect.tsx +17 -40
  28. package/src/components/OwnershipSlider.tsx +102 -0
  29. package/src/components/PhoneNumberInput.tsx +37 -4
  30. package/src/components/SandboxBanner.tsx +92 -0
  31. package/src/components/StaggerIn.tsx +63 -0
  32. package/src/components/StepHeader.tsx +15 -2
  33. package/src/components/StepView.tsx +26 -0
  34. package/src/components/glass/ChromeGlass.tsx +65 -0
  35. package/src/components/glass/FloatingSheet.tsx +242 -0
  36. package/src/components/glass/GlassSheet.tsx +50 -0
  37. package/src/components/glass/GlassSurface.tsx +31 -3
  38. package/src/components/stepHeaderMeta.tsx +3 -1
  39. package/src/components/viewfinder/ImmersiveBottomBar.tsx +28 -17
  40. package/src/components/viewfinder/ImmersiveControls.tsx +26 -14
  41. package/src/components/viewfinder/ViewfinderControls.tsx +29 -7
  42. package/src/config/business.ts +6 -6
  43. package/src/config/businessDetailsValidity.ts +52 -0
  44. package/src/config/businessPrefill.ts +72 -0
  45. package/src/config/businessSteps.ts +14 -3
  46. package/src/config/keyPeople.ts +111 -7
  47. package/src/config/keyPeopleCorporate.ts +19 -0
  48. package/src/config/keyPeopleOwnerTypes.ts +12 -0
  49. package/src/config/keyPeopleOwners.ts +26 -0
  50. package/src/config/keyPeoplePrefill.ts +138 -0
  51. package/src/config/keyPeopleSectionDefs.ts +65 -0
  52. package/src/config/keyPeopleSections.ts +167 -0
  53. package/src/config/registrationHint.ts +1 -1
  54. package/src/config/stepOrder.ts +24 -4
  55. package/src/config/theme.ts +8 -2
  56. package/src/config/website.ts +36 -0
  57. package/src/config/workflowMerge.ts +5 -0
  58. package/src/emrtd/activeAuth.ts +100 -0
  59. package/src/emrtd/extras.ts +76 -0
  60. package/src/emrtd/files.ts +5 -0
  61. package/src/emrtd/index.ts +1 -0
  62. package/src/emrtd/read.ts +8 -1
  63. package/src/emrtd/session.ts +41 -0
  64. package/src/emrtd/stages.ts +11 -0
  65. package/src/lib/contact-recovery.ts +44 -0
  66. package/src/lib/multi-id.ts +210 -0
  67. package/src/lib/resubmit.ts +130 -0
  68. package/src/lib/screen-corners.ts +50 -0
  69. package/src/lib/step-log.ts +24 -3
  70. package/src/lib/use-multi-id-plan.ts +29 -0
  71. package/src/liveness/useModelReady.ts +65 -0
  72. package/src/liveness/visionCameraFaceDetector.ts +35 -0
  73. package/src/screens/ApplicantRoleStep.tsx +9 -5
  74. package/src/screens/BusinessCheckPanel.tsx +89 -0
  75. package/src/screens/BusinessDetailsFields.tsx +109 -0
  76. package/src/screens/BusinessDetailsStep.tsx +131 -145
  77. package/src/screens/BusinessDocumentSlot.tsx +2 -3
  78. package/src/screens/BusinessDocumentsStep.tsx +1 -1
  79. package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
  80. package/src/screens/BusinessPickedCard.tsx +86 -0
  81. package/src/screens/BusinessPickedSection.tsx +51 -0
  82. package/src/screens/BusinessRegistryPickers.tsx +67 -0
  83. package/src/screens/BusinessSandboxToggle.tsx +114 -0
  84. package/src/screens/BusinessSearch.tsx +199 -0
  85. package/src/screens/BusinessSearchResults.tsx +115 -0
  86. package/src/screens/CompanyInfoFields.tsx +86 -51
  87. package/src/screens/ConsentStep.tsx +31 -0
  88. package/src/screens/ContactDestinationField.tsx +1 -0
  89. package/src/screens/ContactVerificationStep.tsx +34 -3
  90. package/src/screens/IdTypeStep.tsx +12 -5
  91. package/src/screens/KeepLinksSheet.tsx +88 -0
  92. package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
  93. package/src/screens/KeyPeopleAwaitList.tsx +126 -0
  94. package/src/screens/KeyPeoplePending.tsx +154 -0
  95. package/src/screens/KeyPeopleSection.tsx +162 -0
  96. package/src/screens/KeyPeopleSectionsList.tsx +103 -0
  97. package/src/screens/KeyPeopleUboExemption.tsx +72 -0
  98. package/src/screens/KeyPersonCard.tsx +36 -4
  99. package/src/screens/KeyPersonForm.tsx +195 -36
  100. package/src/screens/KeyPersonKindToggle.tsx +68 -0
  101. package/src/screens/KeyPersonOwners.tsx +109 -0
  102. package/src/screens/KeyPersonRoleChips.tsx +68 -0
  103. package/src/screens/KeyPersonSheet.tsx +92 -108
  104. package/src/screens/LivenessStep.tsx +18 -0
  105. package/src/screens/NfcStep.tsx +14 -0
  106. package/src/screens/ProofOfAddressStep.tsx +6 -3
  107. package/src/screens/SubmittedStep.tsx +66 -10
  108. package/src/screens/nfc/NfcReadProgress.tsx +1 -0
  109. package/src/screens/useAwaitingPeople.ts +90 -0
  110. package/src/services/api-types.ts +158 -2
  111. package/src/services/api-verify-types.ts +53 -1
  112. package/src/services/api.ts +104 -0
  113. package/src/services/deviceMetadata.ts +1 -1
  114. package/src/services/errors.ts +10 -2
  115. package/src/services/workflowGate.ts +1 -0
  116. package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
  117. package/src/store/businessCheck.ts +113 -0
  118. package/src/store/derive.ts +15 -0
  119. package/src/store/kycStore.ts +241 -14
  120. package/src/store/serverConfig.ts +2 -0
  121. package/src/store/session.ts +208 -0
  122. package/src/store/state.ts +155 -2
  123. package/src/store/submit.ts +63 -13
  124. package/src/store/submitApplicant.ts +5 -11
  125. package/src/types/business.ts +23 -2
  126. package/src/types/config.ts +29 -3
  127. package/src/types/verification.ts +1 -1
  128. package/src/types/workflow.ts +15 -0
  129. package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
@@ -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,159 @@ 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';
47
48
 
48
49
  const workflowBusiness = config.business;
49
50
  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.
51
+ // Precedence mirrors the web SDK: the visitor's pick, then the workflow's
52
+ // PRIMARY country, then the list head.
54
53
  const country = business.country ?? workflowBusiness?.country ?? countries[0] ?? '';
55
54
  const products = useMemo(
56
55
  () => businessProductsForCountry(workflowBusiness, country),
57
56
  [workflowBusiness, country],
58
57
  );
59
- // Re-derived per country rather than remembered: a product the picked country
60
- // does not offer would be rejected at submit.
61
58
  const product = products.includes(business.product ?? '') ? business.product! : products[0]!;
62
59
  const productDef = getBusinessProductDef(product);
63
60
 
64
61
  const modes = companyInfoFieldModes(workflowBusiness);
65
- const companyFields = (Object.keys(modes) as CompanyInfoField[]).filter(
66
- (field) => modes[field] !== 'off',
67
- );
68
- const needsContactEmail = keyPeopleNeedsContactEmail(workflowBusiness);
62
+ const showCompanyInfo = Object.values(modes).some((m) => m !== 'off');
63
+ const showContactEmail = keyPeopleNeedsContactEmail(workflowBusiness);
69
64
 
70
65
  const set = <K extends keyof typeof business>(key: K, value: (typeof business)[K]): void =>
71
66
  store.getState().setBusinessField(key, value);
72
67
 
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
68
  const regHint = registrationNumberHint(country, productDef);
87
69
  const regNumber = business.registrationNumber;
70
+ // A company has been named, so the card replaces the search. The NUMBER is
71
+ // what names it (a prefilled session sends one without a name; the register
72
+ // supplies the name on Continue, so an unnamed card is momentary).
73
+ const picked = regNumber.trim() !== '';
74
+ const nameRequired = workflowBusiness?.requireRegistrationName === true;
88
75
  const formatOk =
89
76
  !regHint.isValidFormat || regNumber.trim() === '' || regHint.isValidFormat(regNumber);
90
77
  const numberValid = regNumber.trim().length >= 2 && formatOk;
91
- const canContinue =
92
- numberValid && !nameMissing && missingCompanyInfo.length === 0 && !contactEmailInvalid;
93
78
 
94
- const handleContinue = (): void => {
95
- setTouched(true);
96
- if (!canContinue) return;
79
+ const isFormValid = businessDetailsValid({
80
+ phase,
81
+ business,
82
+ modes,
83
+ product,
84
+ numberValid,
85
+ nameRequired,
86
+ showContactEmail,
87
+ });
88
+
89
+ const checking = check.status === 'checking';
90
+
91
+ const handleContinue = async (): Promise<void> => {
92
+ if (!isFormValid || checking) return;
97
93
  // 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);
94
+ // was on screen. Only when different: these are identity keys, and writing
95
+ // an unchanged value would needlessly reset the check we just ran.
96
+ if (business.country !== country) set('country', country);
97
+ if (business.product !== product) set('product', product);
98
+
99
+ // The paid registry check — awaited, so the details screen opens already
100
+ // holding what the register said. Only a definitive "not on the register"
101
+ // stops the flow: everything else (a short balance, an outage) continues
102
+ // and is checked at submission, as it was before.
103
+ const { canContinue, company } = await store.getState().checkBusiness();
104
+ if (!canContinue) return;
105
+
106
+ // First Continue ends at the details screen rather than the next step: the
107
+ // register has only just answered, and this is where what it said gets put
108
+ // in front of them to confirm or correct.
109
+ if (phase === 'pick') {
110
+ const { patch, prefilled } = registerPrefillPatch(company, store.getState().business);
111
+ if (prefilled.length > 0) store.getState().applyBusinessPrefill(patch, prefilled);
112
+ setPhase('details');
113
+ return;
114
+ }
101
115
  store.getState().nextStep();
102
116
  };
103
117
 
104
118
  return (
105
119
  <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}
120
+ <BusinessRegistryPickers
121
+ country={country}
122
+ countries={countries}
123
+ product={product}
124
+ products={products}
125
+ onCountry={(code) => {
126
+ set('country', code);
127
+ // The product list narrows per country, so a stale pick is cleared
128
+ // rather than carried into a refused submission.
129
+ set('product', null);
130
+ }}
131
+ onProduct={(key) => set('product', key)}
162
132
  />
163
133
 
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
- />
134
+ {/* HIDDEN, not unmounted, once a company is chosen: unmounting threw
135
+ away the query and results, so "Change" dropped somebody back to an
136
+ empty box. Hiding keeps the picker alive, which makes Change cheap. */}
137
+ <View style={{ display: phase === 'pick' && country && !picked ? 'flex' : 'none' }}>
138
+ <BusinessSearch
139
+ country={country}
140
+ onPicked={(hit) => {
141
+ // Names the company and nothing more: the register is asked on
142
+ // Continue, and the fields it fills live on the screen after.
143
+ set('registrationNumber', hit.registrationNumber);
144
+ set('registrationName', hit.name);
145
+ }}
146
+ onManualEntry={() => setPhase('details')}
147
+ />
148
+ </View>
149
+
150
+ {phase === 'pick' && picked ? (
151
+ <BusinessPickedSection
152
+ country={country}
153
+ name={business.registrationName}
154
+ registrationNumber={regNumber}
155
+ isSandbox={isSandbox}
156
+ onChange={() => {
157
+ // Back to the search rather than an undo: they are changing WHICH
158
+ // company this is about, and the fields belong to the old one.
159
+ set('registrationNumber', '');
160
+ set('registrationName', '');
161
+ }}
162
+ />
163
+ ) : null}
172
164
 
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
- />
165
+ {phase === 'details' ? (
166
+ <BusinessDetailsFields
167
+ business={business}
168
+ productDef={productDef}
169
+ regHint={regHint}
170
+ numberValid={numberValid}
171
+ formatOk={formatOk}
172
+ requireName={nameRequired}
173
+ country={country}
174
+ modes={modes}
175
+ showCompanyInfo={showCompanyInfo}
176
+ showContactEmail={showContactEmail}
177
+ onChange={set}
178
+ />
179
+ ) : null}
186
180
 
187
- {needsContactEmail ? (
181
+ {checkPanelVisible(check.status) ? (
188
182
  <>
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
- />
183
+ <View style={{ height: spacing.lg }} />
184
+ <BusinessCheckPanel check={check} />
200
185
  </>
201
186
  ) : null}
202
187
 
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}
188
+ <View style={{ height: spacing.lg }} />
189
+ <MyazaButton
190
+ label={checking ? 'Checking…' : phase === 'details' ? 'Confirm details & continue' : 'Continue'}
191
+ onPress={() => void handleContinue()}
192
+ loading={checking}
193
+ disabled={!isFormValid || checking}
194
+ />
210
195
  </View>
211
196
  );
212
197
  }
198
+
@@ -5,6 +5,7 @@ import { radius, spacing } from '../config/theme';
5
5
  import { useTheme } from '../components/runtime';
6
6
  import { MyazaText } from '../components/Typography';
7
7
  import { Icon } from '../components/Icon';
8
+ import { DashedBorder } from '../components/DashedBorder';
8
9
 
9
10
  // ---------------------------------------------------------------------------
10
11
  // One business-document upload slot — mirrors the web SDK's
@@ -134,15 +135,13 @@ export function BusinessDocumentSlot({
134
135
  style={{
135
136
  flexDirection: 'row',
136
137
  alignItems: 'center',
137
- borderWidth: 2,
138
- borderStyle: 'dashed',
139
- borderColor: colors.border,
140
138
  borderRadius: radius.sm,
141
139
  padding: spacing.md,
142
140
  marginBottom: spacing.sm + 4,
143
141
  opacity: uploading ? 0.7 : 1,
144
142
  }}
145
143
  >
144
+ <DashedBorder color={colors.border} radius={radius.sm} />
146
145
  {uploading ? (
147
146
  <ActivityIndicator size="small" color={colors.primary} />
148
147
  ) : (
@@ -32,7 +32,7 @@ import { loadDocumentPicker } from '../services/documentPicker';
32
32
  export const businessDocumentsMeta = {
33
33
  title: 'Business documents',
34
34
  description:
35
- 'Upload the supporting documents for your business. Required documents are marked with *.',
35
+ '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
36
  };
37
37
 
38
38
  export function BusinessDocumentsStep(): React.ReactElement {