@myazahq/kyc-sdk-react-native 2.4.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 -1
- package/src/MyazaKYC.tsx +12 -0
- package/src/components/DashedBorder.tsx +55 -0
- package/src/components/DialCodePicker.tsx +14 -7
- package/src/components/Icon.tsx +9 -0
- package/src/components/MultiIdProgress.tsx +107 -0
- package/src/components/MyazaInput.tsx +23 -4
- package/src/components/MyazaSelect.tsx +2 -1
- package/src/components/OwnershipSlider.tsx +102 -0
- package/src/components/PhoneNumberInput.tsx +37 -4
- package/src/components/StaggerIn.tsx +63 -0
- package/src/components/StepView.tsx +26 -0
- package/src/components/glass/FloatingSheet.tsx +73 -21
- package/src/components/glass/GlassSheet.tsx +14 -2
- package/src/components/stepHeaderMeta.tsx +3 -1
- 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 +22 -1
- package/src/types/verification.ts +1 -1
- package/src/types/workflow.ts +15 -0
- package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
|
@@ -1,57 +1,87 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
|
2
2
|
import { View } from 'react-native';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { spacing } from '../config/theme';
|
|
5
5
|
import { useKyc, useKycConfig, useKycStore, useTheme } from '../components/runtime';
|
|
6
6
|
import { MyazaText } from '../components/Typography';
|
|
7
7
|
import { MyazaButton } from '../components/MyazaButton';
|
|
8
|
-
import {
|
|
8
|
+
import { DashedBorder } from '../components/DashedBorder';
|
|
9
|
+
import { KeyPeopleSectionsList } from './KeyPeopleSectionsList';
|
|
9
10
|
import { KeyPersonSheet } from './KeyPersonSheet';
|
|
10
11
|
import { keyPeopleMinEntries } from '../config/businessSteps';
|
|
11
12
|
import {
|
|
13
|
+
keyPeopleRequireEmail,
|
|
12
14
|
emptyKeyPerson,
|
|
13
15
|
invalidKeyPersonRows,
|
|
14
|
-
isKeyPersonRowBlank,
|
|
15
16
|
isKeyPersonRowValid,
|
|
16
17
|
MAX_KEY_PEOPLE_ROWS,
|
|
17
18
|
type KeyPersonEntry,
|
|
18
19
|
} from '../config/keyPeople';
|
|
19
|
-
import type
|
|
20
|
+
import { SECTION_ROLE, type KeyPeopleSection as SectionKey } from '../config/keyPeopleSections';
|
|
21
|
+
import { defaultUboThreshold, keyPeopleSectionList } from '../config/keyPeopleSectionDefs';
|
|
22
|
+
import { prefillKeyPeople, shouldPrefill } from '../config/keyPeoplePrefill';
|
|
23
|
+
import { radius } from '../config/theme';
|
|
20
24
|
|
|
21
25
|
// ---------------------------------------------------------------------------
|
|
22
|
-
//
|
|
26
|
+
// The key-people step, sectioned: Beneficial owners / Shareholders /
|
|
27
|
+
// Directors & representatives, each with a plain-language definition, its own
|
|
28
|
+
// add-tile, and quick-add chips that grant a person already entered another
|
|
29
|
+
// hat. The sections are VIEWS over one shared list (keyPeopleSections.ts):
|
|
30
|
+
// one human can hold several roles, exactly as the register files them, and
|
|
31
|
+
// a stake at the threshold moves them up on screen exactly as the server
|
|
32
|
+
// will escalate them at submit.
|
|
23
33
|
//
|
|
24
|
-
// The registry lookup discovers people too, and the server reconciles the
|
|
25
|
-
// someone the registry names who the applicant did NOT list is flagged
|
|
26
|
-
// `undisclosed
|
|
27
|
-
// merely data entry — what the applicant chooses to omit is evidence.
|
|
28
|
-
//
|
|
29
|
-
// The list stays a clean stack of compact summary cards; the FORM lives in the
|
|
30
|
-
// add/edit sheet (KeyPersonSheet). Tapping a card edits it; "Add a person"
|
|
31
|
-
// opens a fresh sheet. Removal is inside the edit sheet, behind a deliberate
|
|
32
|
-
// tap — never one stray touch on the list.
|
|
34
|
+
// The registry lookup discovers people too, and the server reconciles the
|
|
35
|
+
// two: someone the registry names who the applicant did NOT list is flagged
|
|
36
|
+
// `undisclosed` — what the applicant chooses to omit is evidence.
|
|
33
37
|
// ---------------------------------------------------------------------------
|
|
34
38
|
|
|
35
39
|
export const businessKeyPeopleMeta = {
|
|
36
|
-
title: '
|
|
37
|
-
description:
|
|
38
|
-
"List the company's directors and owners of 25% or more. Each will receive a link to verify their identity.",
|
|
40
|
+
title: 'Key people',
|
|
41
|
+
description: "Add the company's directors, shareholders and beneficial owners.",
|
|
39
42
|
};
|
|
40
43
|
|
|
41
|
-
type SheetState = { mode: 'add'
|
|
44
|
+
type SheetState = { mode: 'add' | 'edit'; section: SectionKey; index?: number } | null;
|
|
42
45
|
|
|
43
46
|
export function BusinessKeyPeopleStep(): React.ReactElement {
|
|
44
47
|
const config = useKycConfig();
|
|
45
48
|
const store = useKycStore();
|
|
46
49
|
const { colors } = useTheme();
|
|
47
50
|
const rows = useKyc((s) => s.businessApplication.keyPeople);
|
|
51
|
+
const uboUnidentifiable = useKyc((s) => s.businessApplication.uboUnidentifiable);
|
|
48
52
|
const registryCountry = useKyc((s) => s.business.country);
|
|
53
|
+
const officers = useKyc((s) => s.businessCheck.officers);
|
|
49
54
|
const [sheet, setSheet] = useState<SheetState>(null);
|
|
50
55
|
|
|
56
|
+
// Start from the register's own officer list (fetched by the paid check at
|
|
57
|
+
// selection), so this is a confirmation rather than a memory test. Once
|
|
58
|
+
// only, and never over anything typed: an applicant who has already entered
|
|
59
|
+
// a name has told us something the register did not.
|
|
60
|
+
const prefilled = useRef(false);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (prefilled.current || officers.length === 0 || !shouldPrefill(rows)) return;
|
|
63
|
+
prefilled.current = true;
|
|
64
|
+
store.getState().setKeyPeople(prefillKeyPeople(officers, registryCountry ?? ''));
|
|
65
|
+
// rows is deliberately out of the dep list: this must run on the arrival
|
|
66
|
+
// of officers, not on every edit the applicant then makes.
|
|
67
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
68
|
+
}, [officers, registryCountry]);
|
|
69
|
+
|
|
51
70
|
const minEntries = keyPeopleMinEntries(config.business);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
// Roles whose email is mandatory (the ones actually sent a verification
|
|
72
|
+
// link) — threaded into every validity read so the card, the sheet and the
|
|
73
|
+
// Continue gate agree on what "complete" means.
|
|
74
|
+
const emailRequiredFor = useMemo(
|
|
75
|
+
() => keyPeopleRequireEmail(config.business),
|
|
76
|
+
[config.business],
|
|
77
|
+
);
|
|
78
|
+
const validCount = rows.filter((r) => isKeyPersonRowValid(r, emailRequiredFor)).length;
|
|
79
|
+
const invalidRows = invalidKeyPersonRows(rows, emailRequiredFor);
|
|
80
|
+
// The same line the server draws: the workflow's own threshold, else the
|
|
81
|
+
// register's default (NG files significant control from a lower bar).
|
|
82
|
+
const threshold =
|
|
83
|
+
config.business?.keyPeople?.ownershipThreshold ??
|
|
84
|
+
defaultUboThreshold(registryCountry || config.business?.country);
|
|
55
85
|
|
|
56
86
|
// Combined ownership above 100% is factually impossible — catch the typo
|
|
57
87
|
// here rather than shipping it into the registry cross-check as a doomed
|
|
@@ -65,15 +95,17 @@ export function BusinessKeyPeopleStep(): React.ReactElement {
|
|
|
65
95
|
const fmtPct = (n: number): string => (Number.isInteger(n) ? String(n) : n.toFixed(1));
|
|
66
96
|
|
|
67
97
|
const canContinue = validCount >= minEntries && invalidRows.length === 0 && !overAllocated;
|
|
98
|
+
const canAdd = rows.length < MAX_KEY_PEOPLE_ROWS;
|
|
68
99
|
|
|
69
100
|
// New people default to the business's registry country (picked on the
|
|
70
101
|
// details step) — most directors are local, and a foreign one just switches
|
|
71
102
|
// theirs. Mirrors the web and Flutter SDKs.
|
|
72
103
|
const defaultCountry = registryCountry ?? config.business?.country ?? '';
|
|
104
|
+
const sections = keyPeopleSectionList(config.business, threshold);
|
|
73
105
|
|
|
74
106
|
const commit = (next: KeyPersonEntry[]): void => store.getState().setKeyPeople(next);
|
|
75
107
|
const handleSave = (entry: KeyPersonEntry): void => {
|
|
76
|
-
if (sheet?.mode === 'edit') {
|
|
108
|
+
if (sheet?.mode === 'edit' && sheet.index != null) {
|
|
77
109
|
commit(rows.map((row, i) => (i === sheet.index ? entry : row)));
|
|
78
110
|
} else {
|
|
79
111
|
commit([...rows, entry]);
|
|
@@ -81,26 +113,28 @@ export function BusinessKeyPeopleStep(): React.ReactElement {
|
|
|
81
113
|
setSheet(null);
|
|
82
114
|
};
|
|
83
115
|
const handleRemove = (): void => {
|
|
84
|
-
if (sheet?.mode === 'edit'
|
|
116
|
+
if (sheet?.mode === 'edit' && sheet.index != null) {
|
|
117
|
+
commit(rows.filter((_, i) => i !== sheet.index));
|
|
118
|
+
}
|
|
85
119
|
setSheet(null);
|
|
86
120
|
};
|
|
87
121
|
|
|
88
|
-
// Indexed access may be undefined under strict indexing — resolved once
|
|
89
|
-
// so the sheet only ever mounts with a real entry.
|
|
90
|
-
const editEntry = sheet?.mode === 'edit' ? rows[sheet.index] : undefined;
|
|
122
|
+
// Indexed access may be undefined under strict indexing — resolved once
|
|
123
|
+
// here so the sheet only ever mounts with a real entry.
|
|
124
|
+
const editEntry = sheet?.mode === 'edit' && sheet.index != null ? rows[sheet.index] : undefined;
|
|
91
125
|
|
|
92
126
|
const handleContinue = (): void => {
|
|
93
127
|
if (!canContinue) return;
|
|
94
128
|
// Half-typed rows are dropped rather than submitted: an entry the user
|
|
95
129
|
// abandoned is not a person they disclosed.
|
|
96
|
-
commit(rows.filter(isKeyPersonRowValid));
|
|
130
|
+
commit(rows.filter((r) => isKeyPersonRowValid(r, emailRequiredFor)));
|
|
97
131
|
store.getState().nextStep();
|
|
98
132
|
};
|
|
99
133
|
|
|
100
134
|
// The dashed hint boxes and their copy mirror the web SDK's step 1:1.
|
|
101
135
|
const hint =
|
|
102
136
|
rows.length === 0 && minEntries === 0
|
|
103
|
-
? "You can skip this if you're unsure
|
|
137
|
+
? "You can skip this if you're unsure. We'll identify directors and owners from the official registry. Adding them here speeds up the review."
|
|
104
138
|
: minEntries > 0 && validCount < minEntries
|
|
105
139
|
? `List at least ${minEntries} ${minEntries === 1 ? 'person' : 'people'} to continue${
|
|
106
140
|
validCount > 0 ? ` (${validCount} of ${minEntries} added)` : ''
|
|
@@ -112,67 +146,62 @@ export function BusinessKeyPeopleStep(): React.ReactElement {
|
|
|
112
146
|
{hint ? (
|
|
113
147
|
<View
|
|
114
148
|
style={{
|
|
115
|
-
borderWidth: 1,
|
|
116
|
-
borderStyle: 'dashed',
|
|
117
|
-
borderColor: colors.border,
|
|
118
149
|
borderRadius: radius.sm,
|
|
119
150
|
backgroundColor: colors.backgroundSecondary,
|
|
120
151
|
padding: spacing.md,
|
|
121
|
-
marginBottom: spacing.
|
|
152
|
+
marginBottom: spacing.lg,
|
|
122
153
|
}}
|
|
123
154
|
>
|
|
124
|
-
<
|
|
155
|
+
<DashedBorder color={colors.border} radius={radius.sm} strokeWidth={1} />
|
|
156
|
+
<MyazaText variant="bodyMedium" color={colors.textSecondary}>
|
|
125
157
|
{hint}
|
|
126
158
|
</MyazaText>
|
|
127
159
|
</View>
|
|
128
160
|
) : null}
|
|
129
161
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
variant="
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
) : (
|
|
149
|
-
<MyazaText variant="bodySmall" color={colors.textMuted} style={{ textAlign: 'center' }}>
|
|
162
|
+
<KeyPeopleSectionsList
|
|
163
|
+
sections={sections}
|
|
164
|
+
rows={rows}
|
|
165
|
+
threshold={threshold}
|
|
166
|
+
emailRequiredFor={emailRequiredFor}
|
|
167
|
+
uboUnidentifiable={uboUnidentifiable}
|
|
168
|
+
canAdd={canAdd}
|
|
169
|
+
onRows={commit}
|
|
170
|
+
onSheet={setSheet}
|
|
171
|
+
onExemption={(next) => store.getState().setUboUnidentifiable(next)}
|
|
172
|
+
/>
|
|
173
|
+
|
|
174
|
+
{!canAdd ? (
|
|
175
|
+
<MyazaText
|
|
176
|
+
variant="bodyMedium"
|
|
177
|
+
color={colors.textMuted}
|
|
178
|
+
style={{ textAlign: 'center', marginTop: spacing.lg }}
|
|
179
|
+
>
|
|
150
180
|
{`You can list up to ${MAX_KEY_PEOPLE_ROWS} people here.`}
|
|
151
181
|
</MyazaText>
|
|
152
|
-
)}
|
|
153
|
-
{/* Breathing room between the add-person affordance and the summary bar —
|
|
154
|
-
matches the Flutter screen's SizedBox after its outline button. */}
|
|
155
|
-
<View style={{ height: spacing.md }} />
|
|
182
|
+
) : null}
|
|
156
183
|
|
|
157
|
-
{/* Total-ownership summary at the DECISION point — the disabled
|
|
158
|
-
button always explains itself, wherever the offending
|
|
184
|
+
{/* Total-ownership summary at the DECISION point — the disabled
|
|
185
|
+
Continue button always explains itself, wherever the offending
|
|
186
|
+
card is. */}
|
|
159
187
|
{totalPct > 0 ? (
|
|
160
188
|
<View
|
|
161
189
|
style={{
|
|
190
|
+
marginTop: spacing.lg,
|
|
162
191
|
flexDirection: 'row',
|
|
163
|
-
justifyContent: 'space-between',
|
|
164
192
|
alignItems: 'center',
|
|
165
|
-
|
|
193
|
+
justifyContent: 'space-between',
|
|
166
194
|
borderRadius: radius.sm,
|
|
195
|
+
backgroundColor: overAllocated ? colors.errorBg : colors.backgroundSecondary,
|
|
167
196
|
paddingHorizontal: spacing.md,
|
|
168
197
|
paddingVertical: spacing.sm + 4,
|
|
169
198
|
}}
|
|
170
199
|
>
|
|
171
|
-
<MyazaText variant="
|
|
200
|
+
<MyazaText variant="bodyMedium" color={overAllocated ? colors.error : colors.textMuted}>
|
|
172
201
|
Total ownership listed
|
|
173
202
|
</MyazaText>
|
|
174
203
|
<MyazaText
|
|
175
|
-
variant="
|
|
204
|
+
variant="bodyMedium"
|
|
176
205
|
color={overAllocated ? colors.error : colors.textDark}
|
|
177
206
|
style={{ fontWeight: '700' }}
|
|
178
207
|
>
|
|
@@ -181,19 +210,24 @@ export function BusinessKeyPeopleStep(): React.ReactElement {
|
|
|
181
210
|
</View>
|
|
182
211
|
) : null}
|
|
183
212
|
{overAllocated ? (
|
|
184
|
-
<MyazaText variant="
|
|
185
|
-
{`Together the percentages can't exceed 100
|
|
213
|
+
<MyazaText variant="bodyMedium" color={colors.error} style={{ marginTop: spacing.xs }}>
|
|
214
|
+
{`Together the percentages can't exceed 100%, so reduce them by ${fmtPct(totalPct - 100)}%.`}
|
|
186
215
|
</MyazaText>
|
|
187
216
|
) : null}
|
|
188
217
|
|
|
189
|
-
<View style={{ height: spacing.
|
|
218
|
+
<View style={{ height: spacing.lg }} />
|
|
190
219
|
<MyazaButton label="Continue" onPress={handleContinue} disabled={!canContinue} />
|
|
191
220
|
|
|
192
221
|
{sheet && (sheet.mode === 'add' || editEntry) ? (
|
|
193
222
|
<KeyPersonSheet
|
|
223
|
+
emailRequiredFor={emailRequiredFor}
|
|
194
224
|
mode={sheet.mode}
|
|
195
|
-
|
|
196
|
-
|
|
225
|
+
section={sheet.section}
|
|
226
|
+
corporateKyb={config.business?.keyPeople?.corporateKyb?.enabled === true}
|
|
227
|
+
initial={
|
|
228
|
+
editEntry ?? { ...emptyKeyPerson(SECTION_ROLE[sheet.section]), country: defaultCountry }
|
|
229
|
+
}
|
|
230
|
+
uboThreshold={threshold}
|
|
197
231
|
otherPctTotal={editEntry ? totalPct - pctOf(editEntry) : totalPct}
|
|
198
232
|
onSave={handleSave}
|
|
199
233
|
onRemove={sheet.mode === 'edit' ? handleRemove : undefined}
|
|
@@ -203,5 +237,3 @@ export function BusinessKeyPeopleStep(): React.ReactElement {
|
|
|
203
237
|
</View>
|
|
204
238
|
);
|
|
205
239
|
}
|
|
206
|
-
|
|
207
|
-
export type { KeyPersonRole };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { CountryFlag } from '../components/CountryFlag';
|
|
5
|
+
import { Icon } from '../components/Icon';
|
|
6
|
+
import { MyazaText } from '../components/Typography';
|
|
7
|
+
import { useTheme } from '../components/runtime';
|
|
8
|
+
import { radius, spacing } from '../config/theme';
|
|
9
|
+
|
|
10
|
+
// The company the applicant picked, standing where the search box was.
|
|
11
|
+
//
|
|
12
|
+
// Choosing a row IS the choice: the card replaces the list, and Change swaps
|
|
13
|
+
// back. A separate confirm button meant two primary buttons on screen at once,
|
|
14
|
+
// and the card already shows what was picked — the confirmation is the screen
|
|
15
|
+
// after, not a button before it. Dimensions mirror the web SDK's picked card
|
|
16
|
+
// (40px badge, 12px padding and gaps).
|
|
17
|
+
|
|
18
|
+
export function BusinessPickedCard({
|
|
19
|
+
country,
|
|
20
|
+
name,
|
|
21
|
+
registrationNumber,
|
|
22
|
+
onChange,
|
|
23
|
+
}: {
|
|
24
|
+
country: string;
|
|
25
|
+
name: string;
|
|
26
|
+
registrationNumber: string;
|
|
27
|
+
onChange: () => void;
|
|
28
|
+
}): React.ReactElement {
|
|
29
|
+
const { colors } = useTheme();
|
|
30
|
+
return (
|
|
31
|
+
<View>
|
|
32
|
+
<MyazaText variant="label" style={{ marginBottom: spacing.sm }}>
|
|
33
|
+
Business
|
|
34
|
+
</MyazaText>
|
|
35
|
+
<View
|
|
36
|
+
style={{
|
|
37
|
+
flexDirection: 'row',
|
|
38
|
+
alignItems: 'center',
|
|
39
|
+
gap: 12,
|
|
40
|
+
borderWidth: 1,
|
|
41
|
+
borderColor: colors.primary,
|
|
42
|
+
backgroundColor: colors.primary50,
|
|
43
|
+
borderRadius: radius.sm,
|
|
44
|
+
padding: 12,
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{/* The badge: the company as a thing that has been chosen, in the same
|
|
48
|
+
filled-circle language the picked rows of every other list use. */}
|
|
49
|
+
<View
|
|
50
|
+
style={{
|
|
51
|
+
width: 40,
|
|
52
|
+
height: 40,
|
|
53
|
+
borderRadius: 20,
|
|
54
|
+
alignItems: 'center',
|
|
55
|
+
justifyContent: 'center',
|
|
56
|
+
backgroundColor: colors.primary,
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<Icon name="building-2" size={20} color={colors.onPrimary} />
|
|
60
|
+
</View>
|
|
61
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
62
|
+
<MyazaText variant="label" color={colors.textDark} numberOfLines={1}>
|
|
63
|
+
{name.trim() || 'We will confirm the name with the register'}
|
|
64
|
+
</MyazaText>
|
|
65
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, marginTop: 2 }}>
|
|
66
|
+
<CountryFlag country={country} size={14} />
|
|
67
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary}>
|
|
68
|
+
{registrationNumber}
|
|
69
|
+
</MyazaText>
|
|
70
|
+
</View>
|
|
71
|
+
</View>
|
|
72
|
+
<Pressable
|
|
73
|
+
accessibilityRole="button"
|
|
74
|
+
onPress={onChange}
|
|
75
|
+
hitSlop={8}
|
|
76
|
+
style={{ flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: 4 }}
|
|
77
|
+
>
|
|
78
|
+
<Icon name="pencil" size={14} color={colors.textDark} />
|
|
79
|
+
<MyazaText variant="bodyMedium" style={{ fontWeight: '500' }} color={colors.textDark}>
|
|
80
|
+
Change
|
|
81
|
+
</MyazaText>
|
|
82
|
+
</Pressable>
|
|
83
|
+
</View>
|
|
84
|
+
</View>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { 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 { BusinessPickedCard } from './BusinessPickedCard';
|
|
8
|
+
import { BusinessSandboxToggle } from './BusinessSandboxToggle';
|
|
9
|
+
|
|
10
|
+
// The picked-company section: the card, what Continue is about to do, and the
|
|
11
|
+
// dev-only test-result pin. Split from BusinessDetailsStep (200-line rule).
|
|
12
|
+
|
|
13
|
+
export function BusinessPickedSection({
|
|
14
|
+
country,
|
|
15
|
+
name,
|
|
16
|
+
registrationNumber,
|
|
17
|
+
isSandbox,
|
|
18
|
+
onChange,
|
|
19
|
+
}: {
|
|
20
|
+
country: string;
|
|
21
|
+
name: string;
|
|
22
|
+
registrationNumber: string;
|
|
23
|
+
isSandbox: boolean;
|
|
24
|
+
onChange: () => void;
|
|
25
|
+
}): React.ReactElement {
|
|
26
|
+
const { colors } = useTheme();
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<BusinessPickedCard
|
|
30
|
+
country={country}
|
|
31
|
+
name={name}
|
|
32
|
+
registrationNumber={registrationNumber}
|
|
33
|
+
onChange={onChange}
|
|
34
|
+
/>
|
|
35
|
+
<View style={{ height: spacing.lg }} />
|
|
36
|
+
{/* Says what the button is about to do: Continue runs a real lookup
|
|
37
|
+
against the register, which takes a moment and is charged to the
|
|
38
|
+
organisation. "Continue" alone made a paid outbound call look like
|
|
39
|
+
moving to the next page. */}
|
|
40
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary}>
|
|
41
|
+
Continue checks this business against the official register and brings back its details.
|
|
42
|
+
</MyazaText>
|
|
43
|
+
{isSandbox ? (
|
|
44
|
+
<>
|
|
45
|
+
<View style={{ height: spacing.lg }} />
|
|
46
|
+
<BusinessSandboxToggle />
|
|
47
|
+
</>
|
|
48
|
+
) : null}
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { CountryField } from '../components/CountryField';
|
|
5
|
+
import { MyazaSelect } from '../components/MyazaSelect';
|
|
6
|
+
import { MyazaText } from '../components/Typography';
|
|
7
|
+
import { businessCountryName, getBusinessProductDef } from '../config/business';
|
|
8
|
+
import { spacing } from '../config/theme';
|
|
9
|
+
|
|
10
|
+
// The registry pickers: which country's register, and which product to run
|
|
11
|
+
// against it. Rendered only when there is a choice to make. Split from
|
|
12
|
+
// BusinessDetailsStep (200-line rule).
|
|
13
|
+
|
|
14
|
+
export function BusinessRegistryPickers({
|
|
15
|
+
country,
|
|
16
|
+
countries,
|
|
17
|
+
product,
|
|
18
|
+
products,
|
|
19
|
+
onCountry,
|
|
20
|
+
onProduct,
|
|
21
|
+
}: {
|
|
22
|
+
country: string;
|
|
23
|
+
countries: string[];
|
|
24
|
+
product: string;
|
|
25
|
+
products: string[];
|
|
26
|
+
onCountry: (code: string) => void;
|
|
27
|
+
onProduct: (key: string) => void;
|
|
28
|
+
}): React.ReactElement {
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
{countries.length > 1 ? (
|
|
32
|
+
<>
|
|
33
|
+
<MyazaText variant="label" style={{ marginBottom: spacing.sm }}>
|
|
34
|
+
Country of registration
|
|
35
|
+
</MyazaText>
|
|
36
|
+
{/* The SAME sheet as the phone field's dial-code picker — restricted
|
|
37
|
+
to the workflow's registry countries. */}
|
|
38
|
+
<CountryField
|
|
39
|
+
value={country}
|
|
40
|
+
options={countries.map((code) => ({ code, name: businessCountryName(code) }))}
|
|
41
|
+
onChange={onCountry}
|
|
42
|
+
/>
|
|
43
|
+
<View style={{ height: spacing.lg }} />
|
|
44
|
+
</>
|
|
45
|
+
) : null}
|
|
46
|
+
|
|
47
|
+
{products.length > 1 ? (
|
|
48
|
+
<>
|
|
49
|
+
<MyazaText variant="label" style={{ marginBottom: spacing.sm }}>
|
|
50
|
+
Verification type
|
|
51
|
+
</MyazaText>
|
|
52
|
+
<MyazaSelect
|
|
53
|
+
value={product}
|
|
54
|
+
sheetTitle="Verification type"
|
|
55
|
+
options={products.map((key) => ({
|
|
56
|
+
value: key,
|
|
57
|
+
label: getBusinessProductDef(key).label,
|
|
58
|
+
}))}
|
|
59
|
+
onChange={onProduct}
|
|
60
|
+
/>
|
|
61
|
+
<View style={{ height: spacing.lg }} />
|
|
62
|
+
</>
|
|
63
|
+
) : null}
|
|
64
|
+
|
|
65
|
+
</>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Animated, Easing, Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { MyazaText } from '../components/Typography';
|
|
5
|
+
import { useKyc, useKycStore, useTheme } from '../components/runtime';
|
|
6
|
+
import { DashedBorder } from '../components/DashedBorder';
|
|
7
|
+
import { spacing } from '../config/theme';
|
|
8
|
+
|
|
9
|
+
// Dev/sandbox only: pick the canned outcome the register check will serve.
|
|
10
|
+
//
|
|
11
|
+
// Chosen BEFORE the lookup runs — repeating the control afterwards would offer
|
|
12
|
+
// to change an answer that has already come back. Production never shows it
|
|
13
|
+
// and never honours a pin if one arrives. Dimensions and motion mirror the web
|
|
14
|
+
// SDK's Test-result control: 40px options in a 2px track, and an indicator
|
|
15
|
+
// that SLIDES between the two — a block that vanishes here and reappears there
|
|
16
|
+
// reads as two separate things blinking; moving it says the selection
|
|
17
|
+
// travelled, which is what actually happened.
|
|
18
|
+
|
|
19
|
+
const OUTCOMES = [
|
|
20
|
+
{ key: 'verified', label: 'Verified' },
|
|
21
|
+
{ key: 'not_found', label: 'Not found' },
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export function BusinessSandboxToggle(): React.ReactElement {
|
|
25
|
+
const store = useKycStore();
|
|
26
|
+
const { colors } = useTheme();
|
|
27
|
+
const outcome = useKyc((s) => s.business.sandboxOutcome) || 'verified';
|
|
28
|
+
const [trackWidth, setTrackWidth] = useState(0);
|
|
29
|
+
// Equal columns are what let the indicator translate by exactly its own
|
|
30
|
+
// width. onLayout reports the BORDER box, so both the 1px borders and the
|
|
31
|
+
// 2px padding come off before halving — sizing off the raw width made the
|
|
32
|
+
// slid indicator touch the track's right edge while the left kept its gap.
|
|
33
|
+
const indicatorWidth = Math.max(0, (trackWidth - 2 * 1 - 2 * 2) / 2);
|
|
34
|
+
const slide = useRef(new Animated.Value(outcome === 'verified' ? 0 : 1)).current;
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
Animated.timing(slide, {
|
|
38
|
+
toValue: outcome === 'verified' ? 0 : 1,
|
|
39
|
+
duration: 200,
|
|
40
|
+
easing: Easing.out(Easing.quad),
|
|
41
|
+
useNativeDriver: true,
|
|
42
|
+
}).start();
|
|
43
|
+
}, [outcome, slide]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<View
|
|
47
|
+
style={{
|
|
48
|
+
borderRadius: 12,
|
|
49
|
+
padding: 12,
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<DashedBorder color={colors.border} radius={12} strokeWidth={1} />
|
|
53
|
+
<MyazaText variant="label">Test result</MyazaText>
|
|
54
|
+
<View style={{ height: spacing.sm }} />
|
|
55
|
+
<View
|
|
56
|
+
onLayout={(e) => setTrackWidth(e.nativeEvent.layout.width)}
|
|
57
|
+
style={{
|
|
58
|
+
flexDirection: 'row',
|
|
59
|
+
borderWidth: 1,
|
|
60
|
+
borderColor: colors.border,
|
|
61
|
+
borderRadius: 8,
|
|
62
|
+
padding: 2,
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
{trackWidth > 0 ? (
|
|
66
|
+
<Animated.View
|
|
67
|
+
pointerEvents="none"
|
|
68
|
+
style={{
|
|
69
|
+
position: 'absolute',
|
|
70
|
+
top: 2,
|
|
71
|
+
bottom: 2,
|
|
72
|
+
left: 2,
|
|
73
|
+
width: indicatorWidth,
|
|
74
|
+
borderRadius: 6,
|
|
75
|
+
backgroundColor: colors.textDark,
|
|
76
|
+
transform: [
|
|
77
|
+
{
|
|
78
|
+
translateX: slide.interpolate({
|
|
79
|
+
inputRange: [0, 1],
|
|
80
|
+
outputRange: [0, indicatorWidth],
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
}}
|
|
85
|
+
/>
|
|
86
|
+
) : null}
|
|
87
|
+
{OUTCOMES.map(({ key, label }) => {
|
|
88
|
+
const active = outcome === key;
|
|
89
|
+
return (
|
|
90
|
+
<Pressable
|
|
91
|
+
key={key}
|
|
92
|
+
accessibilityRole="button"
|
|
93
|
+
accessibilityState={{ selected: active }}
|
|
94
|
+
onPress={() => store.getState().setBusinessField('sandboxOutcome', key)}
|
|
95
|
+
style={{ flex: 1, alignItems: 'center', justifyContent: 'center', height: 40 }}
|
|
96
|
+
>
|
|
97
|
+
{/* Above the indicator, or the label slides out from under it. */}
|
|
98
|
+
<MyazaText
|
|
99
|
+
variant="bodyMedium"
|
|
100
|
+
color={active ? colors.background : colors.textSecondary}
|
|
101
|
+
>
|
|
102
|
+
{label}
|
|
103
|
+
</MyazaText>
|
|
104
|
+
</Pressable>
|
|
105
|
+
);
|
|
106
|
+
})}
|
|
107
|
+
</View>
|
|
108
|
+
<View style={{ height: spacing.sm }} />
|
|
109
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary}>
|
|
110
|
+
Returned instead of calling the register.
|
|
111
|
+
</MyazaText>
|
|
112
|
+
</View>
|
|
113
|
+
);
|
|
114
|
+
}
|