@myazahq/kyc-sdk-react-native 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -5
- package/android/build.gradle +35 -3
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +77 -0
- package/app.plugin.js +89 -8
- package/ios/HybridMyazaFaceDetector.swift +16 -3
- 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 +14 -2
- package/src/MyazaBiometricAuth.tsx +119 -0
- package/src/MyazaKYC.tsx +12 -0
- package/src/components/CountryField.tsx +8 -0
- package/src/components/CountryRegionPicker.tsx +184 -0
- package/src/components/DashedBorder.tsx +55 -0
- package/src/components/DialCodePicker.tsx +53 -67
- package/src/components/DialCodeRow.tsx +102 -0
- package/src/components/FramedMapPicker.tsx +122 -0
- package/src/components/GeoBadge.tsx +34 -0
- package/src/components/Icon.tsx +2 -176
- package/src/components/KycFlow.tsx +60 -32
- package/src/components/LineSkeleton.tsx +100 -0
- package/src/components/MapChrome.tsx +67 -0
- package/src/components/MapPinMarker.tsx +54 -0
- package/src/components/MapPinPicker.tsx +179 -0
- package/src/components/MilestoneTrack.tsx +155 -0
- package/src/components/MultiIdProgress.tsx +107 -0
- package/src/components/MyazaInput.tsx +39 -4
- package/src/components/MyazaSelect.tsx +2 -1
- package/src/components/OptionRow.tsx +22 -3
- package/src/components/OwnershipSlider.tsx +102 -0
- package/src/components/PhoneNumberInput.tsx +45 -4
- package/src/components/PoweredBy.tsx +5 -5
- package/src/components/PresenceBlocks.tsx +153 -0
- package/src/components/StaggerIn.tsx +63 -0
- package/src/components/StepView.tsx +43 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/glass/FloatingSheet.tsx +73 -21
- package/src/components/glass/GlassSheet.tsx +14 -2
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +45 -5
- package/src/config/addressCollection.ts +125 -0
- package/src/config/biometricOptions.ts +100 -0
- package/src/config/business.ts +26 -7
- package/src/config/businessDetailsValidity.ts +52 -0
- package/src/config/businessPrefill.ts +72 -0
- package/src/config/businessSteps.ts +20 -3
- package/src/config/consentStep.ts +19 -0
- 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/proofOfAddress.ts +54 -11
- package/src/config/regions.ts +24 -0
- package/src/config/registrationHint.ts +1 -1
- package/src/config/stepOrder.ts +100 -6
- package/src/config/theme.ts +8 -2
- package/src/config/uploadLimits.ts +35 -0
- package/src/config/website.ts +36 -0
- package/src/config/workflowMerge.ts +16 -5
- 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/index.ts +54 -1
- package/src/lib/address-current-location.ts +171 -0
- package/src/lib/address-field-modes.ts +119 -0
- package/src/lib/address-flow.ts +172 -0
- package/src/lib/address-helpers.ts +48 -0
- package/src/lib/address-line.ts +76 -0
- package/src/lib/address-pin-move.ts +97 -0
- package/src/lib/address-step-recovery.ts +63 -0
- package/src/lib/authed-image.ts +78 -0
- package/src/lib/biometric-auth.ts +52 -0
- package/src/lib/biometric-copy.ts +51 -0
- package/src/lib/captureRing.ts +83 -0
- package/src/lib/contact-recovery.ts +44 -0
- package/src/lib/country-adoption.ts +89 -0
- package/src/lib/inferred-country.ts +65 -0
- package/src/lib/livenessLayout.ts +49 -0
- package/src/lib/map-frame.ts +159 -0
- package/src/lib/map-tiles.ts +143 -0
- package/src/lib/multi-id.ts +210 -0
- package/src/lib/poa-country-gate.ts +27 -0
- package/src/lib/resubmit.ts +130 -0
- package/src/lib/result-copy.ts +116 -0
- package/src/lib/result-wait.ts +53 -0
- package/src/lib/review-map-surface.ts +26 -0
- package/src/lib/scope.ts +31 -0
- package/src/lib/screen-corners.ts +50 -0
- package/src/lib/selfie-upload-wait.ts +79 -0
- package/src/lib/step-log.ts +24 -3
- package/src/lib/street-view-fov.ts +42 -0
- package/src/lib/use-multi-id-plan.ts +29 -0
- package/src/lib/webview-available.ts +47 -0
- package/src/liveness/useLiveness.ts +6 -1
- package/src/liveness/useModelReady.ts +65 -0
- package/src/liveness/visionCameraFaceDetector.ts +35 -0
- package/src/presence/background-math.ts +100 -0
- package/src/presence/background-store.ts +82 -0
- package/src/presence/background.ts +165 -0
- package/src/presence/foreground-service.ts +193 -0
- package/src/presence/fs.ts +51 -0
- package/src/presence/geofence.ts +41 -0
- package/src/presence/math.ts +44 -0
- package/src/presence/post.ts +40 -0
- package/src/presence/report.ts +87 -0
- package/src/presence/sampler.ts +110 -0
- package/src/presence/status.ts +92 -0
- package/src/presence/store.ts +94 -0
- package/src/presence/tier.ts +36 -0
- package/src/presence/watch-wait.ts +81 -0
- package/src/screens/AddressCountryControl.tsx +115 -0
- package/src/screens/ApplicantRoleStep.tsx +9 -5
- package/src/screens/BusinessCheckPanel.tsx +89 -0
- package/src/screens/BusinessDetailsFields.tsx +112 -0
- package/src/screens/BusinessDetailsStep.tsx +133 -145
- package/src/screens/BusinessDocumentSlot.tsx +5 -5
- package/src/screens/BusinessDocumentsStep.tsx +5 -56
- 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 +91 -51
- package/src/screens/ConsentStep.tsx +37 -17
- package/src/screens/ContactActions.tsx +54 -0
- package/src/screens/ContactDestinationField.tsx +5 -0
- package/src/screens/ContactEntryPanel.tsx +75 -0
- package/src/screens/ContactVerificationStep.tsx +43 -42
- package/src/screens/CountrySelectStep.tsx +13 -147
- 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/LivenessAvatar.tsx +15 -3
- package/src/screens/LivenessStep.tsx +172 -52
- package/src/screens/NfcStep.tsx +14 -0
- package/src/screens/PoaDocumentTypeList.tsx +66 -0
- package/src/screens/ProofOfAddressParts.tsx +151 -0
- package/src/screens/ProofOfAddressStep.tsx +58 -96
- package/src/screens/SubmittedBadge.tsx +25 -0
- package/src/screens/SubmittedError.tsx +64 -0
- package/src/screens/SubmittedResult.tsx +117 -0
- package/src/screens/SubmittedStep.tsx +82 -119
- package/src/screens/SubmittedSuccess.tsx +127 -0
- package/src/screens/SubmittedWaiting.tsx +45 -0
- package/src/screens/address/AddressEntranceStep.tsx +161 -0
- package/src/screens/address/AddressIntroGate.tsx +148 -0
- package/src/screens/address/AddressMapStub.tsx +50 -0
- package/src/screens/address/AddressPinStep.tsx +178 -0
- package/src/screens/address/AddressReviewStep.tsx +162 -0
- package/src/screens/address/AddressSandboxTabs.tsx +158 -0
- package/src/screens/address/AddressSearchField.tsx +115 -0
- package/src/screens/address/AddressSearchStep.tsx +94 -0
- package/src/screens/address/CurrentLocationRow.tsx +135 -0
- package/src/screens/address/DetailsSheet.tsx +175 -0
- package/src/screens/address/DetailsSheetFields.tsx +184 -0
- package/src/screens/address/EntranceDropzone.tsx +178 -0
- package/src/screens/address/EntranceFraming.tsx +66 -0
- package/src/screens/address/EntrancePills.tsx +60 -0
- package/src/screens/address/FramedStreetView.tsx +155 -0
- package/src/screens/address/IntroDisclosures.tsx +210 -0
- package/src/screens/address/LabelDecisionRow.tsx +113 -0
- package/src/screens/address/PinSummaryRow.tsx +105 -0
- package/src/screens/address/ReviewAddressBand.tsx +111 -0
- package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
- package/src/screens/address/ReviewMapPicture.tsx +106 -0
- package/src/screens/address/SearchResults.tsx +105 -0
- package/src/screens/address/SearchScreen.tsx +176 -0
- package/src/screens/address/SkipForNow.tsx +43 -0
- package/src/screens/address/StreetViewChrome.tsx +81 -0
- package/src/screens/address/detail-values.ts +22 -0
- package/src/screens/address/fix-source.ts +27 -0
- package/src/screens/address/index.ts +8 -0
- package/src/screens/address/meta.ts +44 -0
- package/src/screens/address/use-address-flow.ts +200 -0
- package/src/screens/address/use-label-pin.ts +80 -0
- package/src/screens/address/use-pin-actions.ts +192 -0
- package/src/screens/biometric/BiometricAuthFlow.tsx +170 -0
- package/src/screens/consent/model.ts +79 -10
- package/src/screens/liveness/CaptureRing.tsx +91 -0
- package/src/screens/liveness/LivenessCamera.tsx +63 -0
- package/src/screens/liveness/LivenessHandover.tsx +43 -0
- package/src/screens/liveness/LivenessOutcome.tsx +4 -3
- package/src/screens/liveness/SelfiePreview.tsx +13 -4
- package/src/screens/liveness/ShutterFlash.tsx +25 -0
- package/src/screens/liveness/index.ts +4 -0
- package/src/screens/liveness/useSelfieUpload.ts +23 -3
- package/src/screens/nfc/NfcReadProgress.tsx +1 -0
- package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
- package/src/screens/useAddressPhotoAttach.ts +77 -0
- package/src/screens/useAwaitingPeople.ts +90 -0
- package/src/screens/useBusinessDocumentAttach.ts +93 -0
- package/src/screens/usePoaAttach.ts +12 -15
- package/src/services/api-biometric.ts +37 -0
- package/src/services/api-types-biometric.ts +41 -0
- package/src/services/api-types.ts +251 -3
- package/src/services/api-verify-types.ts +90 -1
- package/src/services/api.ts +237 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/errors.ts +10 -2
- package/src/services/location.ts +234 -0
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +8 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
- package/src/store/address.ts +97 -0
- package/src/store/businessCheck.ts +113 -0
- package/src/store/derive.ts +44 -4
- package/src/store/kycStore.ts +352 -16
- package/src/store/serverConfig.ts +20 -0
- package/src/store/session.ts +227 -0
- package/src/store/state.ts +289 -2
- package/src/store/submit.ts +80 -13
- package/src/store/submitApplicant.ts +5 -11
- package/src/types/business.ts +23 -2
- package/src/types/config.ts +70 -2
- package/src/types/verification.ts +13 -1
- package/src/types/workflow.ts +112 -1
- package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { MyazaInput } from '../components/MyazaInput';
|
|
5
|
+
import { spacing } from '../config/theme';
|
|
6
|
+
import { CompanyInfoFields } from './CompanyInfoFields';
|
|
7
|
+
import { isValidContactEmail } from '../config/contact';
|
|
8
|
+
import type { BusinessState } from '../store/state';
|
|
9
|
+
import type { BusinessProductDef } from '../config/business';
|
|
10
|
+
import type { RegistrationHint } from '../config/registrationHint';
|
|
11
|
+
import type { CompanyInfoField, CompanyInfoMode } from '../types/business';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// The DETAILS screen of the business step: confirming what the register said.
|
|
15
|
+
//
|
|
16
|
+
// Registration number and name are editable here too — this is also where
|
|
17
|
+
// manual entry lands, for the companies no search index carries. Split from
|
|
18
|
+
// BusinessDetailsStep (200-line rule); field order mirrors the web SDK.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export function BusinessDetailsFields({
|
|
22
|
+
business,
|
|
23
|
+
productDef,
|
|
24
|
+
regHint,
|
|
25
|
+
numberValid,
|
|
26
|
+
formatOk,
|
|
27
|
+
requireName,
|
|
28
|
+
country,
|
|
29
|
+
geoCountry,
|
|
30
|
+
modes,
|
|
31
|
+
showCompanyInfo,
|
|
32
|
+
showContactEmail,
|
|
33
|
+
onChange,
|
|
34
|
+
}: {
|
|
35
|
+
business: BusinessState;
|
|
36
|
+
productDef: BusinessProductDef;
|
|
37
|
+
regHint: RegistrationHint;
|
|
38
|
+
numberValid: boolean;
|
|
39
|
+
formatOk: boolean;
|
|
40
|
+
requireName: boolean;
|
|
41
|
+
country: string;
|
|
42
|
+
geoCountry?: string | null;
|
|
43
|
+
modes: Record<CompanyInfoField, CompanyInfoMode>;
|
|
44
|
+
showCompanyInfo: boolean;
|
|
45
|
+
showContactEmail: boolean;
|
|
46
|
+
onChange: <K extends keyof BusinessState>(key: K, value: BusinessState[K]) => void;
|
|
47
|
+
}): React.ReactElement {
|
|
48
|
+
const regNumber = business.registrationNumber;
|
|
49
|
+
const contactEmail = business.contactEmail;
|
|
50
|
+
const contactEmailInvalid =
|
|
51
|
+
contactEmail.trim() !== '' && !isValidContactEmail(contactEmail.trim());
|
|
52
|
+
return (
|
|
53
|
+
<View>
|
|
54
|
+
<MyazaInput
|
|
55
|
+
label={productDef.inputLabel}
|
|
56
|
+
value={regNumber}
|
|
57
|
+
onChangeText={(text) => onChange('registrationNumber', text)}
|
|
58
|
+
placeholder={regHint.placeholder}
|
|
59
|
+
autoCapitalize="characters"
|
|
60
|
+
autoCorrect={false}
|
|
61
|
+
// Live like the web SDK: a wrong CAC prefix is corrected on the spot,
|
|
62
|
+
// not discovered on Continue. The registry tip fills the same slot
|
|
63
|
+
// until there is an error to show.
|
|
64
|
+
error={
|
|
65
|
+
regNumber !== '' && !numberValid
|
|
66
|
+
? (!formatOk && regHint.formatError) ||
|
|
67
|
+
`Enter a valid ${productDef.inputLabel.toLowerCase()}.`
|
|
68
|
+
: null
|
|
69
|
+
}
|
|
70
|
+
helper={regHint.tip ?? undefined}
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
<View style={{ height: spacing.md }} />
|
|
74
|
+
<MyazaInput
|
|
75
|
+
label={`Registered business name${requireName ? '' : ' (optional)'}`}
|
|
76
|
+
value={business.registrationName}
|
|
77
|
+
onChangeText={(text) => onChange('registrationName', text)}
|
|
78
|
+
placeholder="Enter the registered business name"
|
|
79
|
+
autoCorrect={false}
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
{showCompanyInfo ? (
|
|
83
|
+
<>
|
|
84
|
+
<View style={{ height: spacing.md }} />
|
|
85
|
+
<CompanyInfoFields
|
|
86
|
+
values={business}
|
|
87
|
+
modes={modes}
|
|
88
|
+
country={country}
|
|
89
|
+
geoCountry={geoCountry}
|
|
90
|
+
onChange={(field, value) => onChange(field, value)}
|
|
91
|
+
/>
|
|
92
|
+
</>
|
|
93
|
+
) : null}
|
|
94
|
+
|
|
95
|
+
{showContactEmail ? (
|
|
96
|
+
<>
|
|
97
|
+
<View style={{ height: spacing.md }} />
|
|
98
|
+
<MyazaInput
|
|
99
|
+
label="Contact email for owner verification (optional)"
|
|
100
|
+
value={contactEmail}
|
|
101
|
+
onChangeText={(text) => onChange('contactEmail', text)}
|
|
102
|
+
placeholder="admin@company.com"
|
|
103
|
+
keyboardType="email-address"
|
|
104
|
+
autoCapitalize="none"
|
|
105
|
+
helper="We'll email this address a link for your directors and owners to verify their identity."
|
|
106
|
+
error={contactEmailInvalid ? 'Enter a valid email address.' : null}
|
|
107
|
+
/>
|
|
108
|
+
</>
|
|
109
|
+
) : null}
|
|
110
|
+
</View>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -2,34 +2,33 @@ import React, { useMemo, useState } from 'react';
|
|
|
2
2
|
import { View } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { spacing } from '../config/theme';
|
|
5
|
-
import { useKyc, useKycConfig, useKycStore
|
|
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,161 @@ export const businessDetailsMeta = {
|
|
|
41
40
|
export function BusinessDetailsStep(): React.ReactElement {
|
|
42
41
|
const config = useKycConfig();
|
|
43
42
|
const store = useKycStore();
|
|
44
|
-
const { colors } = useTheme();
|
|
45
43
|
const business = useKyc((s) => s.business);
|
|
46
|
-
const
|
|
44
|
+
const check = useKyc((s) => s.businessCheck);
|
|
45
|
+
const [phase, setPhase] = useState<'pick' | 'details'>('pick');
|
|
46
|
+
// Production never shows the test-result toggle, and never honours a pin.
|
|
47
|
+
const isSandbox = useKyc((s) => s.serverConfig.environment) !== 'PRODUCTION';
|
|
48
|
+
const geoCountry = useKyc((s) => s.serverConfig.geoCountry);
|
|
47
49
|
|
|
48
50
|
const workflowBusiness = config.business;
|
|
49
51
|
const countries = useMemo(() => businessCountriesFor(workflowBusiness), [workflowBusiness]);
|
|
50
|
-
// Precedence mirrors the web SDK
|
|
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.
|
|
52
|
+
// Precedence mirrors the web SDK: the visitor's pick, then the workflow's
|
|
53
|
+
// PRIMARY country, then the list head.
|
|
54
54
|
const country = business.country ?? workflowBusiness?.country ?? countries[0] ?? '';
|
|
55
55
|
const products = useMemo(
|
|
56
56
|
() => businessProductsForCountry(workflowBusiness, country),
|
|
57
57
|
[workflowBusiness, country],
|
|
58
58
|
);
|
|
59
|
-
// Re-derived per country rather than remembered: a product the picked country
|
|
60
|
-
// does not offer would be rejected at submit.
|
|
61
59
|
const product = products.includes(business.product ?? '') ? business.product! : products[0]!;
|
|
62
60
|
const productDef = getBusinessProductDef(product);
|
|
63
61
|
|
|
64
62
|
const modes = companyInfoFieldModes(workflowBusiness);
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
);
|
|
68
|
-
const needsContactEmail = keyPeopleNeedsContactEmail(workflowBusiness);
|
|
63
|
+
const showCompanyInfo = Object.values(modes).some((m) => m !== 'off');
|
|
64
|
+
const showContactEmail = keyPeopleNeedsContactEmail(workflowBusiness);
|
|
69
65
|
|
|
70
66
|
const set = <K extends keyof typeof business>(key: K, value: (typeof business)[K]): void =>
|
|
71
67
|
store.getState().setBusinessField(key, value);
|
|
72
68
|
|
|
73
|
-
const missingCompanyInfo = missingRequiredCompanyInfo(workflowBusiness, {
|
|
74
|
-
address: business.address,
|
|
75
|
-
email: business.email,
|
|
76
|
-
phone: business.phone,
|
|
77
|
-
website: business.website,
|
|
78
|
-
});
|
|
79
|
-
const contactEmailInvalid =
|
|
80
|
-
needsContactEmail && business.contactEmail.trim() !== '' && !isValidContactEmail(business.contactEmail);
|
|
81
|
-
const nameMissing =
|
|
82
|
-
workflowBusiness?.requireRegistrationName === true && business.registrationName.trim() === '';
|
|
83
|
-
|
|
84
|
-
// Country-aware registration-number guidance (NG: CAC prefix rules + format
|
|
85
|
-
// validation; elsewhere: placeholder + registry tip). Mirrors the web SDK.
|
|
86
69
|
const regHint = registrationNumberHint(country, productDef);
|
|
87
70
|
const regNumber = business.registrationNumber;
|
|
71
|
+
// A company has been named, so the card replaces the search. The NUMBER is
|
|
72
|
+
// what names it (a prefilled session sends one without a name; the register
|
|
73
|
+
// supplies the name on Continue, so an unnamed card is momentary).
|
|
74
|
+
const picked = regNumber.trim() !== '';
|
|
75
|
+
const nameRequired = workflowBusiness?.requireRegistrationName === true;
|
|
88
76
|
const formatOk =
|
|
89
77
|
!regHint.isValidFormat || regNumber.trim() === '' || regHint.isValidFormat(regNumber);
|
|
90
78
|
const numberValid = regNumber.trim().length >= 2 && formatOk;
|
|
91
|
-
const canContinue =
|
|
92
|
-
numberValid && !nameMissing && missingCompanyInfo.length === 0 && !contactEmailInvalid;
|
|
93
79
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
80
|
+
const isFormValid = businessDetailsValid({
|
|
81
|
+
phase,
|
|
82
|
+
business,
|
|
83
|
+
modes,
|
|
84
|
+
product,
|
|
85
|
+
numberValid,
|
|
86
|
+
nameRequired,
|
|
87
|
+
showContactEmail,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const checking = check.status === 'checking';
|
|
91
|
+
|
|
92
|
+
const handleContinue = async (): Promise<void> => {
|
|
93
|
+
if (!isFormValid || checking) return;
|
|
97
94
|
// Persist the resolved country/product so the submission uses exactly what
|
|
98
|
-
// was on screen
|
|
99
|
-
|
|
100
|
-
set('
|
|
95
|
+
// was on screen. Only when different: these are identity keys, and writing
|
|
96
|
+
// an unchanged value would needlessly reset the check we just ran.
|
|
97
|
+
if (business.country !== country) set('country', country);
|
|
98
|
+
if (business.product !== product) set('product', product);
|
|
99
|
+
|
|
100
|
+
// The paid registry check — awaited, so the details screen opens already
|
|
101
|
+
// holding what the register said. Only a definitive "not on the register"
|
|
102
|
+
// stops the flow: everything else (a short balance, an outage) continues
|
|
103
|
+
// and is checked at submission, as it was before.
|
|
104
|
+
const { canContinue, company } = await store.getState().checkBusiness();
|
|
105
|
+
if (!canContinue) return;
|
|
106
|
+
|
|
107
|
+
// First Continue ends at the details screen rather than the next step: the
|
|
108
|
+
// register has only just answered, and this is where what it said gets put
|
|
109
|
+
// in front of them to confirm or correct.
|
|
110
|
+
if (phase === 'pick') {
|
|
111
|
+
const { patch, prefilled } = registerPrefillPatch(company, store.getState().business);
|
|
112
|
+
if (prefilled.length > 0) store.getState().applyBusinessPrefill(patch, prefilled);
|
|
113
|
+
setPhase('details');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
101
116
|
store.getState().nextStep();
|
|
102
117
|
};
|
|
103
118
|
|
|
104
119
|
return (
|
|
105
120
|
<View>
|
|
106
|
-
|
|
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}
|
|
121
|
+
<BusinessRegistryPickers
|
|
122
|
+
country={country}
|
|
123
|
+
countries={countries}
|
|
124
|
+
product={product}
|
|
125
|
+
products={products}
|
|
126
|
+
onCountry={(code) => {
|
|
127
|
+
set('country', code);
|
|
128
|
+
// The product list narrows per country, so a stale pick is cleared
|
|
129
|
+
// rather than carried into a refused submission.
|
|
130
|
+
set('product', null);
|
|
131
|
+
}}
|
|
132
|
+
onProduct={(key) => set('product', key)}
|
|
162
133
|
/>
|
|
163
134
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
135
|
+
{/* HIDDEN, not unmounted, once a company is chosen: unmounting threw
|
|
136
|
+
away the query and results, so "Change" dropped somebody back to an
|
|
137
|
+
empty box. Hiding keeps the picker alive, which makes Change cheap. */}
|
|
138
|
+
<View style={{ display: phase === 'pick' && country && !picked ? 'flex' : 'none' }}>
|
|
139
|
+
<BusinessSearch
|
|
140
|
+
country={country}
|
|
141
|
+
onPicked={(hit) => {
|
|
142
|
+
// Names the company and nothing more: the register is asked on
|
|
143
|
+
// Continue, and the fields it fills live on the screen after.
|
|
144
|
+
set('registrationNumber', hit.registrationNumber);
|
|
145
|
+
set('registrationName', hit.name);
|
|
146
|
+
}}
|
|
147
|
+
onManualEntry={() => setPhase('details')}
|
|
148
|
+
/>
|
|
149
|
+
</View>
|
|
150
|
+
|
|
151
|
+
{phase === 'pick' && picked ? (
|
|
152
|
+
<BusinessPickedSection
|
|
153
|
+
country={country}
|
|
154
|
+
name={business.registrationName}
|
|
155
|
+
registrationNumber={regNumber}
|
|
156
|
+
isSandbox={isSandbox}
|
|
157
|
+
onChange={() => {
|
|
158
|
+
// Back to the search rather than an undo: they are changing WHICH
|
|
159
|
+
// company this is about, and the fields belong to the old one.
|
|
160
|
+
set('registrationNumber', '');
|
|
161
|
+
set('registrationName', '');
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
164
|
+
) : null}
|
|
172
165
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
166
|
+
{phase === 'details' ? (
|
|
167
|
+
<BusinessDetailsFields
|
|
168
|
+
business={business}
|
|
169
|
+
productDef={productDef}
|
|
170
|
+
regHint={regHint}
|
|
171
|
+
numberValid={numberValid}
|
|
172
|
+
formatOk={formatOk}
|
|
173
|
+
requireName={nameRequired}
|
|
174
|
+
country={country}
|
|
175
|
+
geoCountry={geoCountry}
|
|
176
|
+
modes={modes}
|
|
177
|
+
showCompanyInfo={showCompanyInfo}
|
|
178
|
+
showContactEmail={showContactEmail}
|
|
179
|
+
onChange={set}
|
|
180
|
+
/>
|
|
181
|
+
) : null}
|
|
186
182
|
|
|
187
|
-
{
|
|
183
|
+
{checkPanelVisible(check.status) ? (
|
|
188
184
|
<>
|
|
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
|
-
/>
|
|
185
|
+
<View style={{ height: spacing.lg }} />
|
|
186
|
+
<BusinessCheckPanel check={check} />
|
|
200
187
|
</>
|
|
201
188
|
) : null}
|
|
202
189
|
|
|
203
|
-
<View style={{ height: spacing.
|
|
204
|
-
<MyazaButton
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
190
|
+
<View style={{ height: spacing.lg }} />
|
|
191
|
+
<MyazaButton
|
|
192
|
+
label={checking ? 'Checking…' : phase === 'details' ? 'Confirm details & continue' : 'Continue'}
|
|
193
|
+
onPress={() => void handleContinue()}
|
|
194
|
+
loading={checking}
|
|
195
|
+
disabled={!isFormValid || checking}
|
|
196
|
+
/>
|
|
210
197
|
</View>
|
|
211
198
|
);
|
|
212
199
|
}
|
|
200
|
+
|
|
@@ -2,15 +2,17 @@ import React from 'react';
|
|
|
2
2
|
import { ActivityIndicator, Image, Pressable, View } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { radius, spacing } from '../config/theme';
|
|
5
|
+
import { UPLOAD_HINT } from '../config/uploadLimits';
|
|
5
6
|
import { useTheme } from '../components/runtime';
|
|
6
7
|
import { MyazaText } from '../components/Typography';
|
|
7
8
|
import { Icon } from '../components/Icon';
|
|
9
|
+
import { DashedBorder } from '../components/DashedBorder';
|
|
8
10
|
|
|
9
11
|
// ---------------------------------------------------------------------------
|
|
10
12
|
// One business-document upload slot — mirrors the web SDK's
|
|
11
13
|
// BusinessDocumentSlot 1:1: empty, it is a dashed full-width tap target
|
|
12
14
|
// (upload glyph — a spinner while uploading — label with a red * when
|
|
13
|
-
// required,
|
|
15
|
+
// required, the shared upload hint); filled, a solid card showing a 48px
|
|
14
16
|
// THUMBNAIL of what was actually picked (a muted file tile for PDFs, a check
|
|
15
17
|
// when the preview didn't survive a remount — web's exact fallback ladder),
|
|
16
18
|
// the file's name over the slot label, Replace, and remove. Showing the file
|
|
@@ -134,15 +136,13 @@ export function BusinessDocumentSlot({
|
|
|
134
136
|
style={{
|
|
135
137
|
flexDirection: 'row',
|
|
136
138
|
alignItems: 'center',
|
|
137
|
-
borderWidth: 2,
|
|
138
|
-
borderStyle: 'dashed',
|
|
139
|
-
borderColor: colors.border,
|
|
140
139
|
borderRadius: radius.sm,
|
|
141
140
|
padding: spacing.md,
|
|
142
141
|
marginBottom: spacing.sm + 4,
|
|
143
142
|
opacity: uploading ? 0.7 : 1,
|
|
144
143
|
}}
|
|
145
144
|
>
|
|
145
|
+
<DashedBorder color={colors.border} radius={radius.sm} />
|
|
146
146
|
{uploading ? (
|
|
147
147
|
<ActivityIndicator size="small" color={colors.primary} />
|
|
148
148
|
) : (
|
|
@@ -159,7 +159,7 @@ export function BusinessDocumentSlot({
|
|
|
159
159
|
) : null}
|
|
160
160
|
</MyazaText>
|
|
161
161
|
<MyazaText variant="bodySmall" color={colors.textMuted}>
|
|
162
|
-
{uploading ? 'Uploading…' :
|
|
162
|
+
{uploading ? 'Uploading…' : UPLOAD_HINT}
|
|
163
163
|
</MyazaText>
|
|
164
164
|
</View>
|
|
165
165
|
</Pressable>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { useCallback, useMemo, useState } from 'react';
|
|
2
2
|
import { View } from 'react-native';
|
|
3
|
-
import * as ImagePicker from 'expo-image-picker';
|
|
4
3
|
|
|
5
4
|
import { spacing } from '../config/theme';
|
|
6
5
|
import { useKyc, useKycConfig, useKycStore, useTheme } from '../components/runtime';
|
|
@@ -11,8 +10,7 @@ import { BusinessDocumentSlot } from './BusinessDocumentSlot';
|
|
|
11
10
|
import { withRetry } from '../services/retry';
|
|
12
11
|
import { compressDocumentImage } from '../services/mediaCompress';
|
|
13
12
|
import { resolveBusinessDocumentTypes, type ResolvedBusinessDocumentType } from '../config/businessSteps';
|
|
14
|
-
import {
|
|
15
|
-
import { loadDocumentPicker } from '../services/documentPicker';
|
|
13
|
+
import { useBusinessDocumentAttach } from './useBusinessDocumentAttach';
|
|
16
14
|
|
|
17
15
|
// ---------------------------------------------------------------------------
|
|
18
16
|
// Supporting company documents (certificate of incorporation, MEMART, …).
|
|
@@ -26,13 +24,14 @@ import { loadDocumentPicker } from '../services/documentPicker';
|
|
|
26
24
|
// Layout mirrors the web SDK 1:1: the header carries the description, each
|
|
27
25
|
// document is a dashed upload slot (see BusinessDocumentSlot), and Continue is
|
|
28
26
|
// disabled until every required slot is filled. Tapping a slot opens the
|
|
29
|
-
// photo/file source sheet — the mobile stand-in for web's file input
|
|
27
|
+
// photo/file source sheet — the mobile stand-in for web's file input; the
|
|
28
|
+
// pickers themselves live in useBusinessDocumentAttach.
|
|
30
29
|
// ---------------------------------------------------------------------------
|
|
31
30
|
|
|
32
31
|
export const businessDocumentsMeta = {
|
|
33
32
|
title: 'Business documents',
|
|
34
33
|
description:
|
|
35
|
-
'Upload the supporting documents for your business. Required documents are marked with *.',
|
|
34
|
+
'Upload the supporting documents for your business. Each one must clearly show the registered business name and registration number. Required documents are marked with *.',
|
|
36
35
|
};
|
|
37
36
|
|
|
38
37
|
export function BusinessDocumentsStep(): React.ReactElement {
|
|
@@ -79,57 +78,7 @@ export function BusinessDocumentsStep(): React.ReactElement {
|
|
|
79
78
|
[store],
|
|
80
79
|
);
|
|
81
80
|
|
|
82
|
-
const takePhoto =
|
|
83
|
-
async (slot: ResolvedBusinessDocumentType) => {
|
|
84
|
-
const permission = await ImagePicker.requestCameraPermissionsAsync();
|
|
85
|
-
if (!permission.granted) {
|
|
86
|
-
setError('Camera access is needed to photograph the document.');
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const result = await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], quality: 1 });
|
|
90
|
-
const asset = result.canceled ? undefined : result.assets[0];
|
|
91
|
-
if (asset) {
|
|
92
|
-
// A FRIENDLY name, not the picker's temp junk — camera/library assets
|
|
93
|
-
// carry generated names; the slot key says what the file IS.
|
|
94
|
-
await attach(slot, asset.uri, asset.mimeType ?? 'image/jpeg', `${slot.key}.jpg`);
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
[attach],
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
const choosePhoto = useCallback(
|
|
101
|
-
async (slot: ResolvedBusinessDocumentType) => {
|
|
102
|
-
const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], quality: 1 });
|
|
103
|
-
const asset = result.canceled ? undefined : result.assets[0];
|
|
104
|
-
if (asset) {
|
|
105
|
-
await attach(slot, asset.uri, asset.mimeType ?? 'image/jpeg', `${slot.key}.jpg`);
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
[attach],
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
const chooseFile = useCallback(
|
|
112
|
-
async (slot: ResolvedBusinessDocumentType) => {
|
|
113
|
-
const picker = loadDocumentPicker();
|
|
114
|
-
if (!picker) {
|
|
115
|
-
setError('Choosing a file is not available in this app. Please photograph the document.');
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const result = await picker.getDocumentAsync({
|
|
119
|
-
type: [...POA_ACCEPTED_MIME_TYPES],
|
|
120
|
-
copyToCacheDirectory: true,
|
|
121
|
-
multiple: false,
|
|
122
|
-
});
|
|
123
|
-
const asset = result.canceled ? undefined : result.assets?.[0];
|
|
124
|
-
if (!asset) return;
|
|
125
|
-
if (!isAcceptedPoaMimeType(asset.mimeType)) {
|
|
126
|
-
setError('Please choose a photo (JPEG, PNG or WebP) or a PDF.');
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
await attach(slot, asset.uri, asset.mimeType, asset.name ?? slot.key);
|
|
130
|
-
},
|
|
131
|
-
[attach],
|
|
132
|
-
);
|
|
81
|
+
const { takePhoto, choosePhoto, chooseFile } = useBusinessDocumentAttach(attach, setError);
|
|
133
82
|
|
|
134
83
|
const handleContinue = (): void => {
|
|
135
84
|
if (missing.length > 0 || busySlot !== null) return;
|