@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,30 @@
|
|
|
1
|
+
import { KYCApiError } from './api';
|
|
2
|
+
import { UPLOAD_HINT } from '../config/uploadLimits';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Why an upload failed, in words the person can act on.
|
|
6
|
+
//
|
|
7
|
+
// Every upload used to fail as "we could not upload that document", which tells
|
|
8
|
+
// the applicant nothing about whether to pick a smaller file, a different
|
|
9
|
+
// format, or simply try again on better signal. The server already says which
|
|
10
|
+
// it is; this reads its answer and hands back the sentence that matches.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/** The refusal, as something the applicant can do something about. */
|
|
14
|
+
export function uploadFailureMessage(error: unknown): string {
|
|
15
|
+
const status = error instanceof KYCApiError ? error.statusCode : null;
|
|
16
|
+
const raw = error instanceof Error ? error.message.toLowerCase() : '';
|
|
17
|
+
|
|
18
|
+
if (status === 413 || raw.includes('too large')) {
|
|
19
|
+
return `That file is too large. ${UPLOAD_HINT}`;
|
|
20
|
+
}
|
|
21
|
+
if (raw.includes('mimetype') || raw.includes('mime type') || raw.includes('unsupported')) {
|
|
22
|
+
return `That file type is not supported. ${UPLOAD_HINT}`;
|
|
23
|
+
}
|
|
24
|
+
// A 4xx we have no better words for still says the file is the problem, so
|
|
25
|
+
// the person retries with a DIFFERENT one rather than the same one twice.
|
|
26
|
+
if (status !== null && status >= 400 && status < 500) {
|
|
27
|
+
return `We could not read that file. ${UPLOAD_HINT}`;
|
|
28
|
+
}
|
|
29
|
+
return 'We could not upload that document. Please check your connection and try again.';
|
|
30
|
+
}
|
|
@@ -102,6 +102,14 @@ export async function resolveWorkflow(
|
|
|
102
102
|
status: 'ready',
|
|
103
103
|
idTypes: res.idTypes,
|
|
104
104
|
branding: res.branding,
|
|
105
|
+
geoCountry: res.geoCountry,
|
|
106
|
+
// The resolution route does not serve these yet, so a workflow mount
|
|
107
|
+
// reads undefined and the address flow offers no search screen. That
|
|
108
|
+
// degrades correctly, and the passthrough means it starts working the
|
|
109
|
+
// day the server carries them.
|
|
110
|
+
addressSearch: res.addressSearch,
|
|
111
|
+
addressSearchMode: res.addressSearchMode,
|
|
112
|
+
mapsFrameUrl: res.mapsFrameUrl ?? null,
|
|
105
113
|
environment: res.environment,
|
|
106
114
|
fatal: false,
|
|
107
115
|
},
|
|
@@ -51,4 +51,33 @@ export interface MyazaFaceDetector extends HybridObject<{ ios: 'swift'; android:
|
|
|
51
51
|
* `faceCount: 0` when no face is present.
|
|
52
52
|
*/
|
|
53
53
|
detectFace(frame: Frame): FaceResult;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether the detector can run RIGHT NOW.
|
|
57
|
+
*
|
|
58
|
+
* Android fetches its model through Google Play Services rather than bundling
|
|
59
|
+
* it (see android/build.gradle), so there is a window — first launch, or a
|
|
60
|
+
* device with no GMS at all — where detection cannot work. This MUST be
|
|
61
|
+
* checked before the camera opens: `detectFace` can only answer in
|
|
62
|
+
* `FaceResult`, where "model missing" and "no face in frame" are the same
|
|
63
|
+
* `faceCount: 0`, so relying on it would leave the user staring at
|
|
64
|
+
* "position your face" forever with nothing to explain why.
|
|
65
|
+
*
|
|
66
|
+
* iOS is always true — Apple Vision is a system framework with nothing to
|
|
67
|
+
* fetch.
|
|
68
|
+
*/
|
|
69
|
+
isModelReady(): boolean;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Ask Play Services to download the model, resolving `true` once it is usable
|
|
73
|
+
* and `false` if it cannot be obtained (no GMS, no network, user declined).
|
|
74
|
+
*
|
|
75
|
+
* Call this EARLY — the SDK primes it at flow start, so the download overlaps
|
|
76
|
+
* the consent and ID-type screens and the model is warm by the time liveness
|
|
77
|
+
* runs. This mirrors the web SDK, which calls `primeFaceMesh()` on mount for
|
|
78
|
+
* exactly the same reason.
|
|
79
|
+
*
|
|
80
|
+
* Safe to call repeatedly; resolves immediately when already ready.
|
|
81
|
+
*/
|
|
82
|
+
prepareModel(): Promise<boolean>;
|
|
54
83
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { AddressState } from './state';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Restoring a saved address pin.
|
|
5
|
+
//
|
|
6
|
+
// The whole address object is written into session progress verbatim, so the
|
|
7
|
+
// snapshot was written by WHATEVER build saved it: coerce every field back to
|
|
8
|
+
// its declared type and degrade to restoring less, never to breaking the flow.
|
|
9
|
+
//
|
|
10
|
+
// Deliberately does NOT restore the device fix (deviceLat / deviceLng /
|
|
11
|
+
// deviceAccuracy / capturedAt). That is the attest-presence reading, and it is
|
|
12
|
+
// taken fresh at confirm — a stale one would claim the applicant stood at the
|
|
13
|
+
// address on a day they did not.
|
|
14
|
+
//
|
|
15
|
+
// Split from session.ts per the 200-line rule. A MIRROR of the web SDK's
|
|
16
|
+
// RESTORE_PROGRESS address branch and the Flutter port.
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
function str(v: unknown): string | null {
|
|
20
|
+
return typeof v === 'string' ? v : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The `parts` breakdown, when the snapshot carries an object for it. */
|
|
24
|
+
function restoreParts(v: unknown): Pick<AddressState, 'parts'> {
|
|
25
|
+
if (!v || typeof v !== 'object') return {};
|
|
26
|
+
const p = v as Record<string, unknown>;
|
|
27
|
+
return {
|
|
28
|
+
parts: {
|
|
29
|
+
street: str(p['street']),
|
|
30
|
+
area: str(p['area']),
|
|
31
|
+
city: str(p['city']),
|
|
32
|
+
state: str(p['state']),
|
|
33
|
+
postcode: str(p['postcode']),
|
|
34
|
+
country: str(p['country']),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The picked-label anchor, only when it is a real coordinate pair. */
|
|
40
|
+
function restorePickedAt(v: unknown): Pick<AddressState, 'pickedAt'> {
|
|
41
|
+
if (!v || typeof v !== 'object') return {};
|
|
42
|
+
const p = v as Record<string, unknown>;
|
|
43
|
+
return typeof p['lat'] === 'number' && typeof p['lng'] === 'number'
|
|
44
|
+
? { pickedAt: { lat: p['lat'], lng: p['lng'] } }
|
|
45
|
+
: {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The Street View frame, only when every field of it survived. A partial
|
|
49
|
+
* frame cannot be re-rendered, and half of one is not a capture. */
|
|
50
|
+
function restoreStreetView(v: unknown): Pick<AddressState, 'streetView'> {
|
|
51
|
+
if (!v || typeof v !== 'object') return {};
|
|
52
|
+
const sv = v as Record<string, unknown>;
|
|
53
|
+
return typeof sv['panoId'] === 'string' &&
|
|
54
|
+
typeof sv['heading'] === 'number' &&
|
|
55
|
+
typeof sv['pitch'] === 'number' &&
|
|
56
|
+
typeof sv['fov'] === 'number'
|
|
57
|
+
? {
|
|
58
|
+
streetView: {
|
|
59
|
+
panoId: sv['panoId'],
|
|
60
|
+
heading: sv['heading'],
|
|
61
|
+
pitch: sv['pitch'],
|
|
62
|
+
fov: sv['fov'],
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
: {};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Rebuild the address from a progress snapshot. The caller has already checked
|
|
70
|
+
* that `lat` and `lng` are numbers — without those there is no pin, and there
|
|
71
|
+
* is nothing to restore.
|
|
72
|
+
*/
|
|
73
|
+
export function restoreAddress(a: Record<string, unknown>): AddressState {
|
|
74
|
+
return {
|
|
75
|
+
lat: a['lat'] as number,
|
|
76
|
+
lng: a['lng'] as number,
|
|
77
|
+
accuracy: typeof a['accuracy'] === 'number' ? a['accuracy'] : null,
|
|
78
|
+
directions: typeof a['directions'] === 'string' ? a['directions'] : '',
|
|
79
|
+
propertyName: typeof a['propertyName'] === 'string' ? a['propertyName'] : '',
|
|
80
|
+
propertyNumber: typeof a['propertyNumber'] === 'string' ? a['propertyNumber'] : '',
|
|
81
|
+
// The resolved line and its breakdown survive a restart too. Dropping them
|
|
82
|
+
// is why a resumed session showed raw coordinates where the applicant had
|
|
83
|
+
// picked an address.
|
|
84
|
+
...(typeof a['label'] === 'string' ? { label: a['label'] } : {}),
|
|
85
|
+
...(typeof a['street'] === 'string' ? { street: a['street'] } : {}),
|
|
86
|
+
// The edit-details claims survive a restart like every other typed field.
|
|
87
|
+
...(typeof a['unit'] === 'string' ? { unit: a['unit'] } : {}),
|
|
88
|
+
...(typeof a['neighbourhood'] === 'string' ? { neighbourhood: a['neighbourhood'] } : {}),
|
|
89
|
+
...(typeof a['city'] === 'string' ? { city: a['city'] } : {}),
|
|
90
|
+
...(typeof a['state'] === 'string' ? { state: a['state'] } : {}),
|
|
91
|
+
...(typeof a['postcode'] === 'string' ? { postcode: a['postcode'] } : {}),
|
|
92
|
+
...(a['labelKept'] === true ? { labelKept: true } : {}),
|
|
93
|
+
...restorePickedAt(a['pickedAt']),
|
|
94
|
+
...restoreParts(a['parts']),
|
|
95
|
+
...restoreStreetView(a['streetView']),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { BusinessCheckResult, KycState } from './state';
|
|
2
|
+
import { EMPTY_BUSINESS_CHECK } from './state';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// The paid registry check at SELECTION, split from kycStore.ts (200-line rule).
|
|
6
|
+
//
|
|
7
|
+
// Run when the applicant confirms their company, so the register answers
|
|
8
|
+
// BEFORE the details screen asks them to confirm what it said — and its key
|
|
9
|
+
// people arrive before that step asks for them. Only a definitive "not on the
|
|
10
|
+
// register" stops the flow: everything else (a short balance, an outage, a
|
|
11
|
+
// spent lookup budget) continues and is checked at submission, exactly as it
|
|
12
|
+
// was before this existed. Mirrors the web SDK's useBusinessCheck — keep the
|
|
13
|
+
// two in lockstep.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
type Set = (partial: Partial<KycState> | ((s: KycState) => Partial<KycState>)) => void;
|
|
17
|
+
|
|
18
|
+
export async function runBusinessCheck(
|
|
19
|
+
get: () => KycState,
|
|
20
|
+
set: Set,
|
|
21
|
+
): Promise<BusinessCheckResult> {
|
|
22
|
+
const s = get();
|
|
23
|
+
const registrationNumber = s.business.registrationNumber?.trim();
|
|
24
|
+
if (!registrationNumber) return { canContinue: true, company: null };
|
|
25
|
+
|
|
26
|
+
// Already checked this exact company — do not pay to be told again.
|
|
27
|
+
//
|
|
28
|
+
// Only a SETTLED answer is reused. 'unavailable' is deliberately not one: an
|
|
29
|
+
// outage said nothing about the company, so a repeat press retries the
|
|
30
|
+
// register rather than replaying the outage. And a remembered answer keeps
|
|
31
|
+
// its meaning — a stored not_found still blocks.
|
|
32
|
+
const normalized = registrationNumber.toUpperCase();
|
|
33
|
+
const settled =
|
|
34
|
+
s.businessCheck.status !== 'idle' &&
|
|
35
|
+
s.businessCheck.status !== 'checking' &&
|
|
36
|
+
s.businessCheck.status !== 'unavailable';
|
|
37
|
+
if (s.businessCheck.checkedNumber === normalized && settled) {
|
|
38
|
+
return {
|
|
39
|
+
canContinue: s.businessCheck.status !== 'not_found',
|
|
40
|
+
company: s.businessCheck.company,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// No session means no anchor for the charge, so there is nothing to run
|
|
45
|
+
// against. The check happens at submission, exactly as it did before.
|
|
46
|
+
if (!s.sessionId) return { canContinue: true, company: null };
|
|
47
|
+
|
|
48
|
+
set((cur) => ({
|
|
49
|
+
businessCheck: { ...cur.businessCheck, status: 'checking', checkedNumber: normalized },
|
|
50
|
+
}));
|
|
51
|
+
try {
|
|
52
|
+
const res = await s.api.businessSelect({
|
|
53
|
+
sessionId: s.sessionId,
|
|
54
|
+
country: s.business.country || s.config.business?.country || '',
|
|
55
|
+
...(s.business.subdivisionCode.trim()
|
|
56
|
+
? { subdivisionCode: s.business.subdivisionCode.trim() }
|
|
57
|
+
: {}),
|
|
58
|
+
...(s.business.registrationName.trim()
|
|
59
|
+
? { registrationName: s.business.registrationName.trim() }
|
|
60
|
+
: {}),
|
|
61
|
+
...(s.business.product ? { product: s.business.product } : {}),
|
|
62
|
+
registrationNumber,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!res.checked) {
|
|
66
|
+
// The organisation could not be charged. Not the applicant's problem and
|
|
67
|
+
// not something they can fix, so it is not shown as an error — the flow
|
|
68
|
+
// continues and the check runs at submission. `lookup_limit_reached` is
|
|
69
|
+
// the one they DID cause, by re-picking company after company, and the
|
|
70
|
+
// one they can act on: check the number rather than keep trying.
|
|
71
|
+
set((cur) => ({
|
|
72
|
+
businessCheck: {
|
|
73
|
+
...cur.businessCheck,
|
|
74
|
+
status: res.reason === 'lookup_limit_reached' ? 'limit_reached' : 'skipped',
|
|
75
|
+
},
|
|
76
|
+
}));
|
|
77
|
+
return { canContinue: true, company: null };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!res.found) {
|
|
81
|
+
// A definitive "not on the register" is worth stopping for: continuing
|
|
82
|
+
// would spend the applicant's time on documents for a company that will
|
|
83
|
+
// fail anyway.
|
|
84
|
+
set((cur) => ({
|
|
85
|
+
businessCheck: { ...cur.businessCheck, status: 'not_found', company: null, officers: [] },
|
|
86
|
+
}));
|
|
87
|
+
return { canContinue: false, company: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { keyPeople, ...company } = res.business!;
|
|
91
|
+
set((cur) => ({
|
|
92
|
+
businessCheck: {
|
|
93
|
+
...cur.businessCheck,
|
|
94
|
+
status: 'found',
|
|
95
|
+
company,
|
|
96
|
+
officers: keyPeople ?? [],
|
|
97
|
+
},
|
|
98
|
+
}));
|
|
99
|
+
return { canContinue: true, company };
|
|
100
|
+
} catch {
|
|
101
|
+
// A register outage is NOT "this company does not exist" — telling the
|
|
102
|
+
// applicant their business is unregistered on the strength of a 503 is the
|
|
103
|
+
// one wrong answer here. Retryable, and it never blocks: the check still
|
|
104
|
+
// happens at submission.
|
|
105
|
+
set((cur) => ({ businessCheck: { ...cur.businessCheck, status: 'unavailable' } }));
|
|
106
|
+
return { canContinue: true, company: null };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Back to square one — called when WHICH company this is about changes. */
|
|
111
|
+
export function resetBusinessCheck(set: Set): void {
|
|
112
|
+
set({ businessCheck: { ...EMPTY_BUSINESS_CHECK } });
|
|
113
|
+
}
|
package/src/store/derive.ts
CHANGED
|
@@ -7,14 +7,19 @@
|
|
|
7
7
|
// keeps a single answer to each of those questions.
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
|
|
10
|
+
import { configScope } from '../lib/scope';
|
|
10
11
|
import { requiresDocumentCapture, supportsNfcChip } from '../config/idTypes';
|
|
11
12
|
import {
|
|
13
|
+
buildStepOrder,
|
|
12
14
|
getStepProgress,
|
|
13
15
|
nextStepInOrder,
|
|
14
16
|
previousStepInOrder,
|
|
15
17
|
type StepOrderOptions,
|
|
16
18
|
} from '../config/stepOrder';
|
|
17
19
|
import { hasProofOfAddressStep } from '../config/proofOfAddress';
|
|
20
|
+
import { addressFlowFor, hasAddressCollectionStep } from '../config/addressCollection';
|
|
21
|
+
import { webViewAvailable } from '../lib/webview-available';
|
|
22
|
+
import { addressVendorsStubbed } from '../lib/address-flow';
|
|
18
23
|
import { businessProductsForCountry, isBusinessFlow } from '../config/business';
|
|
19
24
|
import {
|
|
20
25
|
hasApplicantVerification,
|
|
@@ -22,6 +27,7 @@ import {
|
|
|
22
27
|
hasKeyPeopleCollection,
|
|
23
28
|
} from '../config/businessSteps';
|
|
24
29
|
import { hasActiveQuestionnaire } from '../config/questionnaire';
|
|
30
|
+
import { hasConsentStep, openingStepOf } from '../config/consentStep';
|
|
25
31
|
import { applicantSelfCountry, keyPeoplePayload } from '../config/keyPeople';
|
|
26
32
|
import { featuresFor } from './serverConfig';
|
|
27
33
|
import type { VerifyRequest } from '../services/api';
|
|
@@ -58,6 +64,7 @@ export function stepOrderOptions(state: KycState): StepOrderOptions {
|
|
|
58
64
|
const idType = state.selectedIdType;
|
|
59
65
|
return {
|
|
60
66
|
isBusiness: isBusinessFlow(config),
|
|
67
|
+
scope: configScope(config),
|
|
61
68
|
business: config.business,
|
|
62
69
|
hasDocCapture: idType ? requiresDocumentCapture(idType) : true,
|
|
63
70
|
hasNfc: nfcEnabled(state),
|
|
@@ -72,10 +79,25 @@ export function stepOrderOptions(state: KycState): StepOrderOptions {
|
|
|
72
79
|
hasCountrySelect:
|
|
73
80
|
countrySelectOptions(state).length > 1 &&
|
|
74
81
|
applicantSelfCountry(state.businessApplication) === null,
|
|
82
|
+
hasConsent: hasConsentStep(config),
|
|
75
83
|
hasEmailVerification: config.emailVerification?.enabled === true,
|
|
76
84
|
hasPhoneVerification: config.phoneVerification?.enabled === true,
|
|
77
85
|
hasPoa: hasProofOfAddressStep(config.proofOfAddress),
|
|
86
|
+
hasAddressCollection: hasAddressCollectionStep(config.addressCollection),
|
|
87
|
+
// An absent search flag means no search SCREEN, never an error: the
|
|
88
|
+
// applicant places the pin by hand, the fallback every address failure
|
|
89
|
+
// degrades to.
|
|
90
|
+
addressFlow: addressFlowFor(
|
|
91
|
+
config,
|
|
92
|
+
state.serverConfig.addressSearch === true,
|
|
93
|
+
addressVendorsStubbed({ environment: state.serverConfig.environment }),
|
|
94
|
+
Boolean(state.serverConfig.mapsFrameUrl) && webViewAvailable(),
|
|
95
|
+
),
|
|
78
96
|
hasQuestionnaire: hasActiveQuestionnaire(config.questionnaire),
|
|
97
|
+
// Present only on a session a reviewer sent back. Narrows the order to the
|
|
98
|
+
// steps they ticked, so somebody fixing one blurry photo is not walked
|
|
99
|
+
// through the whole flow again.
|
|
100
|
+
resubmit: config.resubmit,
|
|
79
101
|
};
|
|
80
102
|
}
|
|
81
103
|
|
|
@@ -148,6 +170,15 @@ export function businessSubmission(state: KycState): {
|
|
|
148
170
|
...(text(b.email) ? { email: text(b.email) } : {}),
|
|
149
171
|
...(text(b.phone) ? { phone: text(b.phone) } : {}),
|
|
150
172
|
...(text(b.website) ? { website: text(b.website) } : {}),
|
|
173
|
+
// The five registry facts, sent only when filled. The server drops any
|
|
174
|
+
// the workflow has switched off, so an over-send is validate-and-drop
|
|
175
|
+
// rather than an error, and an under-send is what the required-field
|
|
176
|
+
// 422 is for.
|
|
177
|
+
...(text(b.dateOfIncorporation) ? { dateOfIncorporation: text(b.dateOfIncorporation) } : {}),
|
|
178
|
+
...(text(b.taxId) ? { taxId: text(b.taxId) } : {}),
|
|
179
|
+
...(text(b.vatNumber) ? { vatNumber: text(b.vatNumber) } : {}),
|
|
180
|
+
...(text(b.companyType) ? { companyType: text(b.companyType) } : {}),
|
|
181
|
+
...(text(b.natureOfBusiness) ? { natureOfBusiness: text(b.natureOfBusiness) } : {}),
|
|
151
182
|
// Extras ride along ONLY when the workflow configures them — the server
|
|
152
183
|
// ignores an unconfigured block, but sending one is a claim we did not
|
|
153
184
|
// collect properly.
|
|
@@ -155,6 +186,8 @@ export function businessSubmission(state: KycState): {
|
|
|
155
186
|
? { documents: app.documents.map((d) => ({ type: d.type, mediaId: d.mediaId })) }
|
|
156
187
|
: {}),
|
|
157
188
|
...(hasKeyPeopleCollection(config) && people.length > 0 ? { keyPeople: people } : {}),
|
|
189
|
+
// The FATF fallback, attested: no natural person qualifies as a UBO.
|
|
190
|
+
...(app.uboUnidentifiable ? { uboUnidentifiable: true } : {}),
|
|
158
191
|
...(hasApplicantVerification(config) && app.applicantRole
|
|
159
192
|
? {
|
|
160
193
|
applicant: {
|
|
@@ -222,11 +255,18 @@ export function previousStepBefore(step: KYCStep, state: KycState): KYCStep {
|
|
|
222
255
|
return previousStepInOrder(step, stepOrderOptions(state));
|
|
223
256
|
}
|
|
224
257
|
|
|
258
|
+
/**
|
|
259
|
+
* The step this flow OPENS on: 'consent', or the first real step when the
|
|
260
|
+
* workflow switched the consent screen off (`consentStep: false`). The store
|
|
261
|
+
* starts and resets here, the progress watcher judges "untouched" against it,
|
|
262
|
+
* and the flow hides Back on it. Never depends on anything the applicant has
|
|
263
|
+
* yet to choose: only later steps do.
|
|
264
|
+
*/
|
|
265
|
+
export function openingStep(state: KycState): KYCStep {
|
|
266
|
+
return openingStepOf(buildStepOrder(stepOrderOptions(state)));
|
|
267
|
+
}
|
|
268
|
+
|
|
225
269
|
/** Percentage complete, for the sheet's progress indicator. */
|
|
226
270
|
export function stepProgress(step: KYCStep, state: KycState): number {
|
|
227
271
|
return getStepProgress(step, stepOrderOptions(state));
|
|
228
272
|
}
|
|
229
|
-
|
|
230
|
-
// ---------------------------------------------------------------------------
|
|
231
|
-
// Store factory
|
|
232
|
-
// ---------------------------------------------------------------------------
|