@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
|
@@ -50,11 +50,13 @@ export function ApplicantRoleStep(): React.ReactElement {
|
|
|
50
50
|
const { colors } = useTheme();
|
|
51
51
|
const stored = useKyc((s) => s.businessApplication);
|
|
52
52
|
|
|
53
|
-
// The valid entered
|
|
54
|
-
// is index-based, so the list and the submission can never disagree.
|
|
53
|
+
// The valid entered PEOPLE, keeping their ORIGINAL index — the payload flag
|
|
54
|
+
// is index-based, so the list and the submission can never disagree. A
|
|
55
|
+
// corporate shareholder is excluded: "which of these is you?" is a question
|
|
56
|
+
// about humans, and a company can never be the person filling in the form.
|
|
55
57
|
const people = stored.keyPeople
|
|
56
58
|
.map((row, index) => ({ row, index }))
|
|
57
|
-
.filter(({ row }) => isKeyPersonRowValid(row));
|
|
59
|
+
.filter(({ row }) => !row.isCorporate && isKeyPersonRowValid(row));
|
|
58
60
|
const hasPeople = people.length > 0;
|
|
59
61
|
|
|
60
62
|
const propName = [config.userData?.firstName, config.userData?.lastName]
|
|
@@ -63,13 +65,15 @@ export function ApplicantRoleStep(): React.ReactElement {
|
|
|
63
65
|
|
|
64
66
|
// Tri-state: a person's index, 'other', or nothing chosen yet. First arrival
|
|
65
67
|
// pre-selects the applicant's own entry when the userData name matches
|
|
66
|
-
// (they still confirm) —
|
|
68
|
+
// (they still confirm) — and when the name matches NOBODY listed, "I'm not
|
|
69
|
+
// one of these people" is what that fact means, so it is pre-selected
|
|
70
|
+
// instead (mirrors the web SDK). A stored choice always wins.
|
|
67
71
|
const [selection, setSelection] = useState<number | 'other' | null>(() => {
|
|
68
72
|
if (stored.applicantKeyPersonIndex !== null) return stored.applicantKeyPersonIndex;
|
|
69
73
|
if (stored.applicantRole) return 'other';
|
|
70
74
|
if (propName) {
|
|
71
75
|
const match = people.find(({ row }) => namesLooselyMatch(propName, row.name));
|
|
72
|
-
|
|
76
|
+
return match ? match.index : people.length > 0 ? 'other' : null;
|
|
73
77
|
}
|
|
74
78
|
return null;
|
|
75
79
|
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { 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
|
+
import { MyazaAlert } from '../components/MyazaAlert';
|
|
8
|
+
import type { BusinessCheckState } from '../store/state';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// What the register said, when it was NOT a plain yes.
|
|
12
|
+
//
|
|
13
|
+
// The check is a paid, deliberate step, so it is shown rather than run
|
|
14
|
+
// invisibly: the applicant sees a company that is not on the register caught
|
|
15
|
+
// HERE instead of after documents and a selfie. Nothing is shown for a company
|
|
16
|
+
// that WAS found — the register's answer is already in the form the applicant
|
|
17
|
+
// is looking at, editable, and its officers are the next step. Mirrors the web
|
|
18
|
+
// SDK's BusinessCheckPanel.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export function BusinessCheckPanel({
|
|
22
|
+
check,
|
|
23
|
+
}: {
|
|
24
|
+
check: BusinessCheckState;
|
|
25
|
+
}): React.ReactElement | null {
|
|
26
|
+
// `skipped` shows nothing on purpose. The organisation could not be charged,
|
|
27
|
+
// which is not the applicant's problem and not something they can act on;
|
|
28
|
+
// the check simply happens at submission instead. `checking` shows nothing
|
|
29
|
+
// either — the loader lives inside the Continue button they just pressed.
|
|
30
|
+
if (
|
|
31
|
+
check.status === 'idle' ||
|
|
32
|
+
check.status === 'skipped' ||
|
|
33
|
+
check.status === 'found' ||
|
|
34
|
+
check.status === 'checking'
|
|
35
|
+
) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (check.status === 'not_found') {
|
|
40
|
+
return (
|
|
41
|
+
<MyazaAlert
|
|
42
|
+
variant="warning"
|
|
43
|
+
title="We could not find this business on the register"
|
|
44
|
+
message="Check the registration number and try again."
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (check.status === 'limit_reached') {
|
|
50
|
+
// Not a failure: the submission still runs its own check. What it says is
|
|
51
|
+
// "stop re-picking and look at the number", which is the only thing left
|
|
52
|
+
// that helps.
|
|
53
|
+
return (
|
|
54
|
+
<NeutralPanel
|
|
55
|
+
title="We have stopped looking this up for now"
|
|
56
|
+
body="This application has searched the register several times. Check the registration number is right; your details will still be verified when you submit."
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// unavailable — deliberately not "we could not find it": an outage is not
|
|
62
|
+
// evidence that a business is unregistered.
|
|
63
|
+
return (
|
|
64
|
+
<NeutralPanel
|
|
65
|
+
title="The register is temporarily unavailable"
|
|
66
|
+
body="You can continue, and we will check it shortly."
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function NeutralPanel({ title, body }: { title: string; body: string }): React.ReactElement {
|
|
72
|
+
const { colors } = useTheme();
|
|
73
|
+
return (
|
|
74
|
+
<View
|
|
75
|
+
style={{
|
|
76
|
+
borderWidth: 1,
|
|
77
|
+
borderColor: colors.border,
|
|
78
|
+
borderRadius: radius.sm,
|
|
79
|
+
backgroundColor: colors.backgroundSecondary,
|
|
80
|
+
padding: spacing.md,
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<MyazaText variant="label">{title}</MyazaText>
|
|
84
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: 2 }}>
|
|
85
|
+
{body}
|
|
86
|
+
</MyazaText>
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
modes,
|
|
30
|
+
showCompanyInfo,
|
|
31
|
+
showContactEmail,
|
|
32
|
+
onChange,
|
|
33
|
+
}: {
|
|
34
|
+
business: BusinessState;
|
|
35
|
+
productDef: BusinessProductDef;
|
|
36
|
+
regHint: RegistrationHint;
|
|
37
|
+
numberValid: boolean;
|
|
38
|
+
formatOk: boolean;
|
|
39
|
+
requireName: boolean;
|
|
40
|
+
country: string;
|
|
41
|
+
modes: Record<CompanyInfoField, CompanyInfoMode>;
|
|
42
|
+
showCompanyInfo: boolean;
|
|
43
|
+
showContactEmail: boolean;
|
|
44
|
+
onChange: <K extends keyof BusinessState>(key: K, value: BusinessState[K]) => void;
|
|
45
|
+
}): React.ReactElement {
|
|
46
|
+
const regNumber = business.registrationNumber;
|
|
47
|
+
const contactEmail = business.contactEmail;
|
|
48
|
+
const contactEmailInvalid =
|
|
49
|
+
contactEmail.trim() !== '' && !isValidContactEmail(contactEmail.trim());
|
|
50
|
+
return (
|
|
51
|
+
<View>
|
|
52
|
+
<MyazaInput
|
|
53
|
+
label={productDef.inputLabel}
|
|
54
|
+
value={regNumber}
|
|
55
|
+
onChangeText={(text) => onChange('registrationNumber', text)}
|
|
56
|
+
placeholder={regHint.placeholder}
|
|
57
|
+
autoCapitalize="characters"
|
|
58
|
+
autoCorrect={false}
|
|
59
|
+
// Live like the web SDK: a wrong CAC prefix is corrected on the spot,
|
|
60
|
+
// not discovered on Continue. The registry tip fills the same slot
|
|
61
|
+
// until there is an error to show.
|
|
62
|
+
error={
|
|
63
|
+
regNumber !== '' && !numberValid
|
|
64
|
+
? (!formatOk && regHint.formatError) ||
|
|
65
|
+
`Enter a valid ${productDef.inputLabel.toLowerCase()}.`
|
|
66
|
+
: null
|
|
67
|
+
}
|
|
68
|
+
helper={regHint.tip ?? undefined}
|
|
69
|
+
/>
|
|
70
|
+
|
|
71
|
+
<View style={{ height: spacing.md }} />
|
|
72
|
+
<MyazaInput
|
|
73
|
+
label={`Registered business name${requireName ? '' : ' (optional)'}`}
|
|
74
|
+
value={business.registrationName}
|
|
75
|
+
onChangeText={(text) => onChange('registrationName', text)}
|
|
76
|
+
placeholder="Enter the registered business name"
|
|
77
|
+
autoCorrect={false}
|
|
78
|
+
/>
|
|
79
|
+
|
|
80
|
+
{showCompanyInfo ? (
|
|
81
|
+
<>
|
|
82
|
+
<View style={{ height: spacing.md }} />
|
|
83
|
+
<CompanyInfoFields
|
|
84
|
+
values={business}
|
|
85
|
+
modes={modes}
|
|
86
|
+
country={country}
|
|
87
|
+
onChange={(field, value) => onChange(field, value)}
|
|
88
|
+
/>
|
|
89
|
+
</>
|
|
90
|
+
) : null}
|
|
91
|
+
|
|
92
|
+
{showContactEmail ? (
|
|
93
|
+
<>
|
|
94
|
+
<View style={{ height: spacing.md }} />
|
|
95
|
+
<MyazaInput
|
|
96
|
+
label="Contact email for owner verification (optional)"
|
|
97
|
+
value={contactEmail}
|
|
98
|
+
onChangeText={(text) => onChange('contactEmail', text)}
|
|
99
|
+
placeholder="admin@company.com"
|
|
100
|
+
keyboardType="email-address"
|
|
101
|
+
autoCapitalize="none"
|
|
102
|
+
helper="We'll email this address a link for your directors and owners to verify their identity."
|
|
103
|
+
error={contactEmailInvalid ? 'Enter a valid email address.' : null}
|
|
104
|
+
/>
|
|
105
|
+
</>
|
|
106
|
+
) : null}
|
|
107
|
+
</View>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -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
|
|
6
|
-
import { MyazaText } from '../components/Typography';
|
|
5
|
+
import { useKyc, useKycConfig, useKycStore } from '../components/runtime';
|
|
7
6
|
import { MyazaButton } from '../components/MyazaButton';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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 {
|
|
23
|
-
import
|
|
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
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
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
|
|
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
|
|
51
|
-
//
|
|
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
|
|
66
|
-
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
99
|
-
|
|
100
|
-
set('
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
{
|
|
181
|
+
{checkPanelVisible(check.status) ? (
|
|
188
182
|
<>
|
|
189
|
-
<View style={{ height: spacing.
|
|
190
|
-
<
|
|
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.
|
|
204
|
-
<MyazaButton
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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 {
|