@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.
- package/android/build.gradle +35 -3
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +75 -0
- package/ios/HybridMyazaFaceDetector.swift +8 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
- package/package.json +1 -2
- package/src/MyazaKYC.tsx +12 -0
- package/src/components/BrandBar.tsx +137 -0
- package/src/components/DashedBorder.tsx +55 -0
- package/src/components/DialCodePicker.tsx +25 -24
- package/src/components/DocumentReviewSide.tsx +34 -14
- package/src/components/Icon.tsx +14 -0
- package/src/components/KycSheet.tsx +39 -140
- package/src/components/MediaSourceSheet.tsx +7 -37
- package/src/components/MultiIdProgress.tsx +107 -0
- package/src/components/MyazaDateField.tsx +6 -20
- package/src/components/MyazaInput.tsx +23 -4
- package/src/components/MyazaSelect.tsx +17 -40
- package/src/components/OwnershipSlider.tsx +102 -0
- package/src/components/PhoneNumberInput.tsx +37 -4
- package/src/components/SandboxBanner.tsx +92 -0
- package/src/components/StaggerIn.tsx +63 -0
- package/src/components/StepHeader.tsx +15 -2
- package/src/components/StepView.tsx +26 -0
- package/src/components/glass/ChromeGlass.tsx +65 -0
- package/src/components/glass/FloatingSheet.tsx +242 -0
- package/src/components/glass/GlassSheet.tsx +50 -0
- package/src/components/glass/GlassSurface.tsx +31 -3
- package/src/components/stepHeaderMeta.tsx +3 -1
- package/src/components/viewfinder/ImmersiveBottomBar.tsx +28 -17
- package/src/components/viewfinder/ImmersiveControls.tsx +26 -14
- package/src/components/viewfinder/ViewfinderControls.tsx +29 -7
- package/src/config/business.ts +6 -6
- package/src/config/businessDetailsValidity.ts +52 -0
- package/src/config/businessPrefill.ts +72 -0
- package/src/config/businessSteps.ts +14 -3
- package/src/config/keyPeople.ts +111 -7
- package/src/config/keyPeopleCorporate.ts +19 -0
- package/src/config/keyPeopleOwnerTypes.ts +12 -0
- package/src/config/keyPeopleOwners.ts +26 -0
- package/src/config/keyPeoplePrefill.ts +138 -0
- package/src/config/keyPeopleSectionDefs.ts +65 -0
- package/src/config/keyPeopleSections.ts +167 -0
- package/src/config/registrationHint.ts +1 -1
- package/src/config/stepOrder.ts +24 -4
- package/src/config/theme.ts +8 -2
- package/src/config/website.ts +36 -0
- package/src/config/workflowMerge.ts +5 -0
- package/src/emrtd/activeAuth.ts +100 -0
- package/src/emrtd/extras.ts +76 -0
- package/src/emrtd/files.ts +5 -0
- package/src/emrtd/index.ts +1 -0
- package/src/emrtd/read.ts +8 -1
- package/src/emrtd/session.ts +41 -0
- package/src/emrtd/stages.ts +11 -0
- package/src/lib/contact-recovery.ts +44 -0
- package/src/lib/multi-id.ts +210 -0
- package/src/lib/resubmit.ts +130 -0
- package/src/lib/screen-corners.ts +50 -0
- package/src/lib/step-log.ts +24 -3
- package/src/lib/use-multi-id-plan.ts +29 -0
- package/src/liveness/useModelReady.ts +65 -0
- package/src/liveness/visionCameraFaceDetector.ts +35 -0
- package/src/screens/ApplicantRoleStep.tsx +9 -5
- package/src/screens/BusinessCheckPanel.tsx +89 -0
- package/src/screens/BusinessDetailsFields.tsx +109 -0
- package/src/screens/BusinessDetailsStep.tsx +131 -145
- package/src/screens/BusinessDocumentSlot.tsx +2 -3
- package/src/screens/BusinessDocumentsStep.tsx +1 -1
- package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
- package/src/screens/BusinessPickedCard.tsx +86 -0
- package/src/screens/BusinessPickedSection.tsx +51 -0
- package/src/screens/BusinessRegistryPickers.tsx +67 -0
- package/src/screens/BusinessSandboxToggle.tsx +114 -0
- package/src/screens/BusinessSearch.tsx +199 -0
- package/src/screens/BusinessSearchResults.tsx +115 -0
- package/src/screens/CompanyInfoFields.tsx +86 -51
- package/src/screens/ConsentStep.tsx +31 -0
- package/src/screens/ContactDestinationField.tsx +1 -0
- package/src/screens/ContactVerificationStep.tsx +34 -3
- package/src/screens/IdTypeStep.tsx +12 -5
- package/src/screens/KeepLinksSheet.tsx +88 -0
- package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
- package/src/screens/KeyPeopleAwaitList.tsx +126 -0
- package/src/screens/KeyPeoplePending.tsx +154 -0
- package/src/screens/KeyPeopleSection.tsx +162 -0
- package/src/screens/KeyPeopleSectionsList.tsx +103 -0
- package/src/screens/KeyPeopleUboExemption.tsx +72 -0
- package/src/screens/KeyPersonCard.tsx +36 -4
- package/src/screens/KeyPersonForm.tsx +195 -36
- package/src/screens/KeyPersonKindToggle.tsx +68 -0
- package/src/screens/KeyPersonOwners.tsx +109 -0
- package/src/screens/KeyPersonRoleChips.tsx +68 -0
- package/src/screens/KeyPersonSheet.tsx +92 -108
- package/src/screens/LivenessStep.tsx +18 -0
- package/src/screens/NfcStep.tsx +14 -0
- package/src/screens/ProofOfAddressStep.tsx +6 -3
- package/src/screens/SubmittedStep.tsx +66 -10
- package/src/screens/nfc/NfcReadProgress.tsx +1 -0
- package/src/screens/useAwaitingPeople.ts +90 -0
- package/src/services/api-types.ts +158 -2
- package/src/services/api-verify-types.ts +53 -1
- package/src/services/api.ts +104 -0
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/errors.ts +10 -2
- package/src/services/workflowGate.ts +1 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
- package/src/store/businessCheck.ts +113 -0
- package/src/store/derive.ts +15 -0
- package/src/store/kycStore.ts +241 -14
- package/src/store/serverConfig.ts +2 -0
- package/src/store/session.ts +208 -0
- package/src/store/state.ts +155 -2
- package/src/store/submit.ts +63 -13
- package/src/store/submitApplicant.ts +5 -11
- package/src/types/business.ts +23 -2
- package/src/types/config.ts +29 -3
- package/src/types/verification.ts +1 -1
- package/src/types/workflow.ts +15 -0
- package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
|
@@ -1,27 +1,34 @@
|
|
|
1
|
-
import React, { useMemo } from 'react';
|
|
2
|
-
import { View } from 'react-native';
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
3
|
|
|
4
|
-
import { spacing } from '../config/theme';
|
|
4
|
+
import { radius, spacing } from '../config/theme';
|
|
5
5
|
import { useTheme } from '../components/runtime';
|
|
6
6
|
import { MyazaText } from '../components/Typography';
|
|
7
7
|
import { MyazaInput } from '../components/MyazaInput';
|
|
8
|
-
import {
|
|
8
|
+
import { Icon } from '../components/Icon';
|
|
9
|
+
import { OwnershipSlider } from '../components/OwnershipSlider';
|
|
9
10
|
import { CountryField } from '../components/CountryField';
|
|
10
11
|
import type { DialCodeOption } from '../components/DialCodePicker';
|
|
11
12
|
import { ALL_REGION_CODES, regionCountryName } from '../config/regions';
|
|
12
13
|
import { isValidContactEmail } from '../config/contact';
|
|
14
|
+
import { rowNeedsEmail, type KeyPersonEntry } from '../config/keyPeople';
|
|
13
15
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
type
|
|
17
|
-
} from '../config/
|
|
16
|
+
primaryRole,
|
|
17
|
+
rolesOf,
|
|
18
|
+
type KeyPeopleSection as SectionKey,
|
|
19
|
+
} from '../config/keyPeopleSections';
|
|
18
20
|
import type { KeyPersonRole } from '../types/business';
|
|
21
|
+
import { KeyPersonKindToggle } from './KeyPersonKindToggle';
|
|
22
|
+
import { KeyPersonOwners } from './KeyPersonOwners';
|
|
23
|
+
import { KeyPersonRoleChips } from './KeyPersonRoleChips';
|
|
19
24
|
|
|
20
25
|
// ---------------------------------------------------------------------------
|
|
21
|
-
// The key-person FIELDS
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
26
|
+
// The key-person FIELDS, scoped by the SECTION that opened the sheet: the
|
|
27
|
+
// section already said what this person is, so no coarse role dropdown — the
|
|
28
|
+
// UBO form asks name/stake/country/email, the shareholder form adds the
|
|
29
|
+
// individual-or-company toggle, and the representative form picks between the
|
|
30
|
+
// real classifications (chips) with the human nuance ("CFO, Board Member")
|
|
31
|
+
// captured as a free-text title. Mirrors the web SDK's KeyPersonForm 1:1.
|
|
25
32
|
// ---------------------------------------------------------------------------
|
|
26
33
|
|
|
27
34
|
/** Whether the role is an ownership one (% is only meaningful then). */
|
|
@@ -40,12 +47,22 @@ function allCountryOptions(): DialCodeOption[] {
|
|
|
40
47
|
|
|
41
48
|
export function KeyPersonForm({
|
|
42
49
|
entry,
|
|
50
|
+
section,
|
|
51
|
+
corporateKyb = false,
|
|
43
52
|
onChange,
|
|
44
53
|
uboThreshold = 25,
|
|
45
54
|
combinedPctError = null,
|
|
55
|
+
emailRequiredFor,
|
|
46
56
|
}: {
|
|
47
57
|
entry: KeyPersonEntry;
|
|
58
|
+
/** The section whose add-tile or card opened the sheet. */
|
|
59
|
+
section: SectionKey;
|
|
60
|
+
/** The workflow sends corporate shareholders their own KYB application -
|
|
61
|
+
* the callout must tell that truth instead of the screening-only one. */
|
|
62
|
+
corporateKyb?: boolean;
|
|
48
63
|
onChange: (patch: Partial<KeyPersonEntry>) => void;
|
|
64
|
+
/** Roles whose email is mandatory (they are sent a verification link). */
|
|
65
|
+
emailRequiredFor?: ReadonlySet<KeyPersonRole>;
|
|
49
66
|
/** Ownership % at/above which the server treats a person as a beneficial
|
|
50
67
|
* owner (the workflow's `keyPeople.ownershipThreshold`, default 25). */
|
|
51
68
|
uboThreshold?: number;
|
|
@@ -62,47 +79,97 @@ export function KeyPersonForm({
|
|
|
62
79
|
|
|
63
80
|
const nameInvalid = entry.name !== '' && entry.name.trim().length < 2;
|
|
64
81
|
const emailInvalid = entry.email.trim() !== '' && !isValidContactEmail(entry.email.trim());
|
|
82
|
+
const needsEmail = rowNeedsEmail(entry, emailRequiredFor ?? new Set());
|
|
83
|
+
// COLLAPSED by default so the company sheet stays five fields; open when
|
|
84
|
+
// owners already exist (a restored draft, a look-through find). The
|
|
85
|
+
// capability is one tap away, not one field more. Mirrors the web SDK.
|
|
86
|
+
const [showOwners, setShowOwners] = useState((entry.owners?.length ?? 0) > 0);
|
|
65
87
|
const pct = entry.ownershipPct.trim();
|
|
66
88
|
const pctNum = Number(pct);
|
|
67
89
|
const pctInvalid = pct !== '' && (!Number.isFinite(pctNum) || pctNum < 0 || pctNum > 100);
|
|
68
90
|
// Surface the regulatory consequence as feedback: at/above the threshold the
|
|
69
91
|
// server escalates this person to a beneficial owner regardless of the role
|
|
70
92
|
// picked. Quiet when they already chose UBO — nothing new to say.
|
|
93
|
+
const corp = entry.isCorporate;
|
|
94
|
+
const roles = rolesOf(entry);
|
|
71
95
|
const uboHint =
|
|
72
|
-
!pctInvalid && pct !== '' && pctNum >= uboThreshold &&
|
|
96
|
+
!corp && !pctInvalid && pct !== '' && pctNum >= uboThreshold && !roles.includes('beneficial_owner');
|
|
97
|
+
const belowThresholdNote =
|
|
98
|
+
section === 'ubos' && !pctInvalid && pct !== '' && pctNum > 0 && pctNum < uboThreshold;
|
|
99
|
+
|
|
100
|
+
const setRoles = (next: KeyPersonRole[]): void =>
|
|
101
|
+
onChange({ roles: next, role: primaryRole(next) });
|
|
73
102
|
|
|
74
103
|
return (
|
|
75
104
|
<View>
|
|
105
|
+
{/* Only shareholders can be a company: a beneficial owner is a natural
|
|
106
|
+
person in every regime that defines one, and a representative form
|
|
107
|
+
is about the people who act. */}
|
|
108
|
+
{section === 'shareholders' ? (
|
|
109
|
+
<>
|
|
110
|
+
<KeyPersonKindToggle
|
|
111
|
+
isCorporate={corp}
|
|
112
|
+
onChange={(isCorporate) =>
|
|
113
|
+
// Beneficial ownership is a claim about a person, so switching
|
|
114
|
+
// to a company reads the role set down rather than leaving an
|
|
115
|
+
// impossible one.
|
|
116
|
+
onChange({
|
|
117
|
+
isCorporate,
|
|
118
|
+
...(isCorporate
|
|
119
|
+
? {
|
|
120
|
+
roles: roles.map((r) => (r === 'beneficial_owner' ? 'shareholder' : r)),
|
|
121
|
+
role: primaryRole(
|
|
122
|
+
roles.map((r) => (r === 'beneficial_owner' ? 'shareholder' : r)),
|
|
123
|
+
),
|
|
124
|
+
}
|
|
125
|
+
: { registrationNumber: '', owners: [] }),
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
/>
|
|
129
|
+
<View style={{ height: spacing.sm }} />
|
|
130
|
+
</>
|
|
131
|
+
) : null}
|
|
76
132
|
<MyazaInput
|
|
77
|
-
label=
|
|
133
|
+
label={corp ? 'Company name' : 'Full name'}
|
|
78
134
|
value={entry.name}
|
|
79
135
|
onChangeText={(name) => onChange({ name })}
|
|
80
|
-
placeholder=
|
|
81
|
-
// It's a
|
|
136
|
+
placeholder={corp ? 'e.g. Acme Holdings Ltd' : 'e.g. Bola Owner'}
|
|
137
|
+
// It's a name — start every word capitalized.
|
|
82
138
|
autoCapitalize="words"
|
|
83
|
-
error={
|
|
139
|
+
error={
|
|
140
|
+
nameInvalid
|
|
141
|
+
? `Enter the ${corp ? 'registered company name' : 'person’s full name'}.`
|
|
142
|
+
: null
|
|
143
|
+
}
|
|
84
144
|
/>
|
|
85
145
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
146
|
+
{section === 'representatives' ? (
|
|
147
|
+
<>
|
|
148
|
+
<View style={{ height: spacing.sm }} />
|
|
149
|
+
<KeyPersonRoleChips roles={roles} onRoles={setRoles} />
|
|
150
|
+
</>
|
|
151
|
+
) : null}
|
|
152
|
+
|
|
153
|
+
{!corp ? (
|
|
154
|
+
<>
|
|
155
|
+
<View style={{ height: spacing.sm }} />
|
|
156
|
+
<MyazaInput
|
|
157
|
+
label="Position or title (optional)"
|
|
158
|
+
value={entry.title}
|
|
159
|
+
onChangeText={(title) => onChange({ title })}
|
|
160
|
+
placeholder="e.g. CFO, Board Member"
|
|
161
|
+
autoCapitalize="words"
|
|
162
|
+
autoCorrect={false}
|
|
163
|
+
/>
|
|
164
|
+
</>
|
|
165
|
+
) : null}
|
|
99
166
|
|
|
100
167
|
<View style={{ height: spacing.sm }} />
|
|
101
168
|
<MyazaInput
|
|
102
169
|
label="Ownership % (optional)"
|
|
103
170
|
value={entry.ownershipPct}
|
|
104
171
|
onChangeText={(ownershipPct) => onChange({ ownershipPct })}
|
|
105
|
-
placeholder=
|
|
172
|
+
placeholder="0"
|
|
106
173
|
keyboardType="decimal-pad"
|
|
107
174
|
suffix={
|
|
108
175
|
<MyazaText variant="bodySmall" color={colors.textMuted}>
|
|
@@ -113,15 +180,37 @@ export function KeyPersonForm({
|
|
|
113
180
|
helper={
|
|
114
181
|
uboHint
|
|
115
182
|
? `At ${uboThreshold}% or more, this person counts as a beneficial owner.`
|
|
116
|
-
:
|
|
183
|
+
: belowThresholdNote
|
|
184
|
+
? `Below ${uboThreshold}% they will be recorded as a shareholder.`
|
|
185
|
+
: undefined
|
|
117
186
|
}
|
|
118
187
|
/>
|
|
188
|
+
<View style={{ height: spacing.xs }} />
|
|
189
|
+
{/* The slider rests at 0 for an undeclared stake; only a drag writes a
|
|
190
|
+
value, so an untouched slider still submits "not declared". */}
|
|
191
|
+
<OwnershipSlider
|
|
192
|
+
value={pct !== '' && Number.isFinite(Number(pct)) ? Number(pct) : 0}
|
|
193
|
+
onChange={(next) => onChange({ ownershipPct: String(next) })}
|
|
194
|
+
/>
|
|
195
|
+
|
|
196
|
+
{corp ? (
|
|
197
|
+
<>
|
|
198
|
+
<View style={{ height: spacing.sm }} />
|
|
199
|
+
<MyazaInput
|
|
200
|
+
label="Registration number (optional)"
|
|
201
|
+
value={entry.registrationNumber}
|
|
202
|
+
onChangeText={(registrationNumber) => onChange({ registrationNumber })}
|
|
203
|
+
placeholder="e.g. RC123456"
|
|
204
|
+
autoCapitalize="characters"
|
|
205
|
+
/>
|
|
206
|
+
</>
|
|
207
|
+
) : null}
|
|
119
208
|
|
|
120
209
|
<View style={{ height: spacing.sm }} />
|
|
121
|
-
<MyazaText variant="
|
|
210
|
+
<MyazaText variant="label" style={{ marginBottom: spacing.sm }}>
|
|
122
211
|
Country{' '}
|
|
123
|
-
<MyazaText variant="
|
|
124
|
-
(where their ID was issued)
|
|
212
|
+
<MyazaText variant="bodyMedium" color={colors.textSecondary}>
|
|
213
|
+
{corp ? '(of incorporation)' : '(where their ID was issued)'}
|
|
125
214
|
</MyazaText>
|
|
126
215
|
</MyazaText>
|
|
127
216
|
{/* The SAME sheet as the phone field's dial-code picker (keyboard-aware,
|
|
@@ -133,16 +222,86 @@ export function KeyPersonForm({
|
|
|
133
222
|
onChange={(country) => onChange({ country })}
|
|
134
223
|
/>
|
|
135
224
|
|
|
225
|
+
{corp && !showOwners ? (
|
|
226
|
+
<>
|
|
227
|
+
<View style={{ height: spacing.sm }} />
|
|
228
|
+
<Pressable
|
|
229
|
+
accessibilityRole="button"
|
|
230
|
+
onPress={() => setShowOwners(true)}
|
|
231
|
+
hitSlop={8}
|
|
232
|
+
style={{ flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start' }}
|
|
233
|
+
>
|
|
234
|
+
<Icon name="chevron-right" size={16} color={colors.textSecondary} />
|
|
235
|
+
<MyazaText
|
|
236
|
+
variant="bodyMedium"
|
|
237
|
+
color={colors.textSecondary}
|
|
238
|
+
style={{ fontWeight: '500' }}
|
|
239
|
+
>
|
|
240
|
+
Add the people who own it (optional)
|
|
241
|
+
</MyazaText>
|
|
242
|
+
</Pressable>
|
|
243
|
+
</>
|
|
244
|
+
) : null}
|
|
245
|
+
{corp && showOwners ? (
|
|
246
|
+
<>
|
|
247
|
+
<View style={{ height: spacing.sm }} />
|
|
248
|
+
<KeyPersonOwners
|
|
249
|
+
owners={entry.owners ?? []}
|
|
250
|
+
companyName={entry.name}
|
|
251
|
+
onChange={(owners) => onChange({ owners })}
|
|
252
|
+
/>
|
|
253
|
+
</>
|
|
254
|
+
) : null}
|
|
255
|
+
|
|
136
256
|
<View style={{ height: spacing.sm }} />
|
|
137
257
|
<MyazaInput
|
|
138
|
-
label=
|
|
258
|
+
label={
|
|
259
|
+
needsEmail
|
|
260
|
+
? 'Email *'
|
|
261
|
+
: corp
|
|
262
|
+
? 'Email (optional)'
|
|
263
|
+
: 'Email (optional, used to send their verification link)'
|
|
264
|
+
}
|
|
139
265
|
value={entry.email}
|
|
140
266
|
onChangeText={(email) => onChange({ email })}
|
|
141
267
|
placeholder="name@company.com"
|
|
142
268
|
keyboardType="email-address"
|
|
143
269
|
autoCapitalize="none"
|
|
144
270
|
error={emailInvalid ? 'Enter a valid email address.' : null}
|
|
271
|
+
helper={
|
|
272
|
+
needsEmail && entry.email.trim() === ''
|
|
273
|
+
? 'Required: this is how they receive their own verification link.'
|
|
274
|
+
: undefined
|
|
275
|
+
}
|
|
145
276
|
/>
|
|
277
|
+
|
|
278
|
+
{/* What actually happens to a company on this list, stated as a callout
|
|
279
|
+
rather than buried in a field helper, because it answers the question
|
|
280
|
+
every applicant has at this exact point ("is the company going to be
|
|
281
|
+
asked for a selfie?"). Deliberately OUR facts, not a promise of a
|
|
282
|
+
nested KYB we do not run. Mirrors the web SDK's callout. */}
|
|
283
|
+
{corp ? (
|
|
284
|
+
<View
|
|
285
|
+
style={{
|
|
286
|
+
marginTop: spacing.md,
|
|
287
|
+
flexDirection: 'row',
|
|
288
|
+
alignItems: 'flex-start',
|
|
289
|
+
gap: 10,
|
|
290
|
+
borderRadius: radius.sm,
|
|
291
|
+
backgroundColor: colors.primary50,
|
|
292
|
+
padding: 12,
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
<View style={{ marginTop: 2 }}>
|
|
296
|
+
<Icon name="info" size={16} color={colors.primary} />
|
|
297
|
+
</View>
|
|
298
|
+
<MyazaText variant="bodyMedium" color={colors.textSecondary} style={{ flex: 1 }}>
|
|
299
|
+
{corporateKyb
|
|
300
|
+
? 'This company will need its own KYB verification: it receives a link to a business application of its own, where the people who own it are identified. We also screen it against sanctions lists.'
|
|
301
|
+
: 'A company is never asked to verify an identity. We check it against sanctions lists, and the people who own it are reviewed separately, so list its owners above if you know them.'}
|
|
302
|
+
</MyazaText>
|
|
303
|
+
</View>
|
|
304
|
+
) : null}
|
|
146
305
|
</View>
|
|
147
306
|
);
|
|
148
307
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius, spacing } from '../config/theme';
|
|
5
|
+
import { useTheme } from '../components/runtime';
|
|
6
|
+
import { MyazaText } from '../components/Typography';
|
|
7
|
+
|
|
8
|
+
// Person or company.
|
|
9
|
+
//
|
|
10
|
+
// The first question on the form, because it changes what the rest of it asks:
|
|
11
|
+
// a company has a registration number rather than an ID country, and no
|
|
12
|
+
// identity of its own to verify. Left unasked, a limited company was collected
|
|
13
|
+
// as a person, escalated to beneficial owner (which by definition means a
|
|
14
|
+
// natural person), and sent a link to take a selfie.
|
|
15
|
+
|
|
16
|
+
export function KeyPersonKindToggle({
|
|
17
|
+
isCorporate,
|
|
18
|
+
onChange,
|
|
19
|
+
}: {
|
|
20
|
+
isCorporate: boolean;
|
|
21
|
+
onChange: (isCorporate: boolean) => void;
|
|
22
|
+
}): React.ReactElement {
|
|
23
|
+
const { colors } = useTheme();
|
|
24
|
+
const options: Array<{ value: boolean; label: string }> = [
|
|
25
|
+
{ value: false, label: 'A person' },
|
|
26
|
+
{ value: true, label: 'A company' },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<View>
|
|
31
|
+
<MyazaText variant="bodySmall" style={{ fontWeight: '600', marginBottom: spacing.xs }}>
|
|
32
|
+
Who is this?
|
|
33
|
+
</MyazaText>
|
|
34
|
+
<View style={{ flexDirection: 'row', gap: spacing.sm }}>
|
|
35
|
+
{options.map(({ value, label }) => {
|
|
36
|
+
const selected = isCorporate === value;
|
|
37
|
+
return (
|
|
38
|
+
<Pressable
|
|
39
|
+
key={label}
|
|
40
|
+
accessibilityRole="radio"
|
|
41
|
+
accessibilityState={{ selected }}
|
|
42
|
+
accessibilityLabel={label}
|
|
43
|
+
onPress={() => onChange(value)}
|
|
44
|
+
style={{
|
|
45
|
+
flex: 1,
|
|
46
|
+
height: 44,
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
justifyContent: 'center',
|
|
49
|
+
borderRadius: radius.lg,
|
|
50
|
+
borderWidth: 1,
|
|
51
|
+
borderColor: selected ? colors.primary : colors.border,
|
|
52
|
+
backgroundColor: selected ? `${colors.primary}1A` : 'transparent',
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<MyazaText
|
|
56
|
+
variant="bodySmall"
|
|
57
|
+
style={{ fontWeight: '600' }}
|
|
58
|
+
color={selected ? colors.primary : colors.textSecondary}
|
|
59
|
+
>
|
|
60
|
+
{label}
|
|
61
|
+
</MyazaText>
|
|
62
|
+
</Pressable>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</View>
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing, radius } from '../config/theme';
|
|
5
|
+
import { useTheme } from '../components/runtime';
|
|
6
|
+
import { MyazaText } from '../components/Typography';
|
|
7
|
+
import { MyazaInput } from '../components/MyazaInput';
|
|
8
|
+
import { emptyKeyPersonOwner, type KeyPersonOwnerEntry } from '../config/keyPeople';
|
|
9
|
+
|
|
10
|
+
const MAX_OWNERS = 10;
|
|
11
|
+
|
|
12
|
+
// Who owns this company.
|
|
13
|
+
//
|
|
14
|
+
// A beneficial owner is a natural person, so a corporate shareholder is a
|
|
15
|
+
// branch of the ownership chain that stops at a legal entity. Where the company
|
|
16
|
+
// is registered somewhere we can look up, the server follows it. Where it is
|
|
17
|
+
// not — a foreign parent, an offshore vehicle — this is the only route to the
|
|
18
|
+
// people above it, and asking is better than recording nothing.
|
|
19
|
+
//
|
|
20
|
+
// Deliberately short: a name and a share. Everything else about them is
|
|
21
|
+
// unknowable to the person filling in this form, and a longer list is one
|
|
22
|
+
// people abandon.
|
|
23
|
+
|
|
24
|
+
export function KeyPersonOwners({
|
|
25
|
+
owners,
|
|
26
|
+
onChange,
|
|
27
|
+
companyName,
|
|
28
|
+
}: {
|
|
29
|
+
owners: KeyPersonOwnerEntry[];
|
|
30
|
+
onChange: (owners: KeyPersonOwnerEntry[]) => void;
|
|
31
|
+
companyName: string;
|
|
32
|
+
}): React.ReactElement {
|
|
33
|
+
const { colors } = useTheme();
|
|
34
|
+
const patch = (index: number, next: Partial<KeyPersonOwnerEntry>) =>
|
|
35
|
+
onChange(owners.map((o, i) => (i === index ? { ...o, ...next } : o)));
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<View
|
|
39
|
+
style={{
|
|
40
|
+
borderWidth: 1,
|
|
41
|
+
borderColor: colors.border,
|
|
42
|
+
borderRadius: radius.lg,
|
|
43
|
+
padding: spacing.md,
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<MyazaText variant="bodySmall" style={{ fontWeight: '600' }}>
|
|
47
|
+
Who owns {companyName.trim() || 'this company'}? (optional)
|
|
48
|
+
</MyazaText>
|
|
49
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: 2 }}>
|
|
50
|
+
A company cannot verify an identity, so tell us the people behind it if you know them.
|
|
51
|
+
</MyazaText>
|
|
52
|
+
|
|
53
|
+
{owners.map((owner, index) => (
|
|
54
|
+
<View key={index} style={{ marginTop: spacing.sm }}>
|
|
55
|
+
<MyazaInput
|
|
56
|
+
label={`Owner ${index + 1}`}
|
|
57
|
+
value={owner.name}
|
|
58
|
+
onChangeText={(name) => patch(index, { name })}
|
|
59
|
+
placeholder="Full name"
|
|
60
|
+
autoCapitalize="words"
|
|
61
|
+
/>
|
|
62
|
+
<View style={{ height: spacing.xs }} />
|
|
63
|
+
<MyazaInput
|
|
64
|
+
label="Their share (optional)"
|
|
65
|
+
value={owner.ownershipPct}
|
|
66
|
+
onChangeText={(ownershipPct) => patch(index, { ownershipPct })}
|
|
67
|
+
placeholder="e.g. 75"
|
|
68
|
+
keyboardType="decimal-pad"
|
|
69
|
+
suffix={
|
|
70
|
+
<MyazaText variant="bodySmall" color={colors.textMuted}>
|
|
71
|
+
%
|
|
72
|
+
</MyazaText>
|
|
73
|
+
}
|
|
74
|
+
/>
|
|
75
|
+
<Pressable
|
|
76
|
+
accessibilityRole="button"
|
|
77
|
+
accessibilityLabel={`Remove owner ${index + 1}`}
|
|
78
|
+
onPress={() => onChange(owners.filter((_, i) => i !== index))}
|
|
79
|
+
style={{ paddingVertical: spacing.xs, alignSelf: 'flex-start' }}
|
|
80
|
+
>
|
|
81
|
+
<MyazaText variant="bodySmall" color={colors.error}>
|
|
82
|
+
Remove
|
|
83
|
+
</MyazaText>
|
|
84
|
+
</Pressable>
|
|
85
|
+
</View>
|
|
86
|
+
))}
|
|
87
|
+
|
|
88
|
+
{owners.length < MAX_OWNERS ? (
|
|
89
|
+
<Pressable
|
|
90
|
+
accessibilityRole="button"
|
|
91
|
+
onPress={() => onChange([...owners, emptyKeyPersonOwner()])}
|
|
92
|
+
style={{
|
|
93
|
+
marginTop: spacing.sm,
|
|
94
|
+
height: 40,
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
justifyContent: 'center',
|
|
97
|
+
borderRadius: radius.md,
|
|
98
|
+
borderWidth: 1,
|
|
99
|
+
borderColor: colors.border,
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<MyazaText variant="bodySmall" style={{ fontWeight: '600' }} color={colors.primary}>
|
|
103
|
+
{owners.length === 0 ? 'Add an owner' : 'Add another'}
|
|
104
|
+
</MyazaText>
|
|
105
|
+
</Pressable>
|
|
106
|
+
) : null}
|
|
107
|
+
</View>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { MyazaText } from '../components/Typography';
|
|
5
|
+
import { useTheme } from '../components/runtime';
|
|
6
|
+
import { spacing } from '../config/theme';
|
|
7
|
+
import type { KeyPersonRole } from '../types/business';
|
|
8
|
+
|
|
9
|
+
// The representative form's role chips — real classifications, not job titles
|
|
10
|
+
// (those go in the free-text position field). Multi-select over the entry's
|
|
11
|
+
// role SET, with the last representative hat pinned on: unticking it would
|
|
12
|
+
// silently drop the person from the section they are being edited in.
|
|
13
|
+
// Mirrors the web SDK's KeyPersonRoleChips.
|
|
14
|
+
|
|
15
|
+
const REP_ROLES: Array<{ role: KeyPersonRole; label: string }> = [
|
|
16
|
+
{ role: 'director', label: 'Director' },
|
|
17
|
+
{ role: 'signatory', label: 'Signatory' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function KeyPersonRoleChips({
|
|
21
|
+
roles,
|
|
22
|
+
onRoles,
|
|
23
|
+
}: {
|
|
24
|
+
roles: KeyPersonRole[];
|
|
25
|
+
onRoles: (next: KeyPersonRole[]) => void;
|
|
26
|
+
}): React.ReactElement {
|
|
27
|
+
const { colors } = useTheme();
|
|
28
|
+
const repCount = roles.filter((r) => r === 'director' || r === 'signatory').length;
|
|
29
|
+
return (
|
|
30
|
+
<View>
|
|
31
|
+
<MyazaText variant="label" style={{ marginBottom: spacing.sm }}>
|
|
32
|
+
Role
|
|
33
|
+
</MyazaText>
|
|
34
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
|
|
35
|
+
{REP_ROLES.map(({ role, label }) => {
|
|
36
|
+
const active = roles.includes(role);
|
|
37
|
+
return (
|
|
38
|
+
<Pressable
|
|
39
|
+
key={role}
|
|
40
|
+
accessibilityRole="button"
|
|
41
|
+
accessibilityState={{ selected: active }}
|
|
42
|
+
onPress={() => {
|
|
43
|
+
if (active && repCount <= 1) return;
|
|
44
|
+
onRoles(active ? roles.filter((r) => r !== role) : [...roles, role]);
|
|
45
|
+
}}
|
|
46
|
+
style={{
|
|
47
|
+
borderRadius: 999,
|
|
48
|
+
borderWidth: 1,
|
|
49
|
+
borderColor: active ? colors.primary : colors.border,
|
|
50
|
+
backgroundColor: active ? colors.primary : 'transparent',
|
|
51
|
+
paddingHorizontal: 14,
|
|
52
|
+
paddingVertical: 7,
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<MyazaText
|
|
56
|
+
variant="bodyMedium"
|
|
57
|
+
color={active ? colors.onPrimary : colors.textSecondary}
|
|
58
|
+
style={{ fontWeight: '500' }}
|
|
59
|
+
>
|
|
60
|
+
{label}
|
|
61
|
+
</MyazaText>
|
|
62
|
+
</Pressable>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</View>
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|