@myazahq/kyc-sdk-react-native 2.5.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/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +6 -4
- package/app.plugin.js +89 -8
- package/ios/HybridMyazaFaceDetector.swift +8 -3
- package/package.json +14 -2
- package/src/MyazaBiometricAuth.tsx +119 -0
- package/src/components/CountryField.tsx +8 -0
- package/src/components/CountryRegionPicker.tsx +184 -0
- package/src/components/DialCodePicker.tsx +47 -68
- 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 -185
- 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/MyazaInput.tsx +17 -1
- package/src/components/OptionRow.tsx +22 -3
- package/src/components/PhoneNumberInput.tsx +8 -0
- package/src/components/PoweredBy.tsx +5 -5
- package/src/components/PresenceBlocks.tsx +153 -0
- package/src/components/StepView.tsx +17 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +42 -4
- package/src/config/addressCollection.ts +125 -0
- package/src/config/biometricOptions.ts +100 -0
- package/src/config/business.ts +20 -1
- package/src/config/businessSteps.ts +8 -2
- package/src/config/consentStep.ts +19 -0
- package/src/config/proofOfAddress.ts +54 -11
- package/src/config/regions.ts +24 -0
- package/src/config/stepOrder.ts +79 -5
- package/src/config/uploadLimits.ts +35 -0
- package/src/config/workflowMerge.ts +11 -5
- 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/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/poa-country-gate.ts +27 -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/selfie-upload-wait.ts +79 -0
- package/src/lib/street-view-fov.ts +42 -0
- package/src/lib/webview-available.ts +47 -0
- package/src/liveness/useLiveness.ts +6 -1
- 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/BusinessDetailsFields.tsx +3 -0
- package/src/screens/BusinessDetailsStep.tsx +2 -0
- package/src/screens/BusinessDocumentSlot.tsx +3 -2
- package/src/screens/BusinessDocumentsStep.tsx +4 -55
- package/src/screens/CompanyInfoFields.tsx +6 -1
- package/src/screens/ConsentStep.tsx +6 -17
- package/src/screens/ContactActions.tsx +54 -0
- package/src/screens/ContactDestinationField.tsx +4 -0
- package/src/screens/ContactEntryPanel.tsx +75 -0
- package/src/screens/ContactVerificationStep.tsx +26 -56
- package/src/screens/CountrySelectStep.tsx +13 -147
- package/src/screens/LivenessAvatar.tsx +15 -3
- package/src/screens/LivenessStep.tsx +154 -52
- package/src/screens/PoaDocumentTypeList.tsx +66 -0
- package/src/screens/ProofOfAddressParts.tsx +151 -0
- package/src/screens/ProofOfAddressStep.tsx +58 -99
- 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 +71 -164
- 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/NfcScanIllustration.tsx +7 -18
- package/src/screens/useAddressPhotoAttach.ts +77 -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 +93 -1
- package/src/services/api-verify-types.ts +37 -0
- package/src/services/api.ts +133 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/location.ts +234 -0
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +7 -0
- package/src/store/address.ts +97 -0
- package/src/store/derive.ts +29 -4
- package/src/store/kycStore.ts +112 -3
- package/src/store/serverConfig.ts +18 -0
- package/src/store/session.ts +23 -4
- package/src/store/state.ts +134 -0
- package/src/store/submit.ts +18 -1
- package/src/types/config.ts +48 -1
- package/src/types/verification.ts +12 -0
- package/src/types/workflow.ts +97 -1
|
@@ -87,8 +87,14 @@ export type BusinessSectionStep =
|
|
|
87
87
|
export function businessSectionSteps(
|
|
88
88
|
business: WorkflowBusinessConfig | undefined,
|
|
89
89
|
withQuestionnaire = false,
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
withAddressCollection = false,
|
|
91
|
+
): (BusinessSectionStep | 'questionnaire' | 'address-collection')[] {
|
|
92
|
+
const steps: (BusinessSectionStep | 'questionnaire' | 'address-collection')[] = [
|
|
93
|
+
'business-details',
|
|
94
|
+
];
|
|
95
|
+
// The premises pin follows the company details it is about — before the
|
|
96
|
+
// paperwork, and long before the application hands over to other people.
|
|
97
|
+
if (withAddressCollection) steps.push('address-collection');
|
|
92
98
|
// Documents BEFORE key people: they are about the company the applicant has
|
|
93
99
|
// just identified, so they follow that thread, and the register's officer
|
|
94
100
|
// list - which the key-people step is a confirmation of - is what should
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { KYCStep } from '../types/config';
|
|
2
|
+
|
|
3
|
+
// ─── The consent screen switch (`consentStep`) ──────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// A workflow can switch the opening consent (welcome) screen off, for a host
|
|
6
|
+
// app that has already asked: the flow then opens on its first real step.
|
|
7
|
+
// Mirrors the server's `WorkflowConfigSchema.consentStep` and the web /
|
|
8
|
+
// Flutter helpers of the same name; keep the default in lockstep.
|
|
9
|
+
|
|
10
|
+
/** Whether the flow opens on the consent screen. Absent = yes. */
|
|
11
|
+
export function hasConsentStep(config: { consentStep?: boolean | null }): boolean {
|
|
12
|
+
return config.consentStep !== false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The step a built order opens on; 'consent' for an empty order, which no
|
|
16
|
+
* flow produces. */
|
|
17
|
+
export function openingStepOf(order: readonly KYCStep[]): KYCStep {
|
|
18
|
+
return order[0] ?? 'consent';
|
|
19
|
+
}
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
// server is going to make anyway.
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
|
|
11
|
-
import type { PoaDocumentType, ProofOfAddressConfig } from '../types/workflow';
|
|
11
|
+
import type { PoaDocumentType, PoaNameRule, ProofOfAddressConfig } from '../types/workflow';
|
|
12
12
|
|
|
13
13
|
export const POA_TYPE_LABELS: Record<PoaDocumentType, string> = {
|
|
14
14
|
utility_bill: 'Utility bill',
|
|
15
15
|
bank_statement: 'Bank statement',
|
|
16
16
|
tenancy_agreement: 'Tenancy agreement',
|
|
17
|
+
government_document: 'Government-issued document',
|
|
17
18
|
other: 'Other document',
|
|
18
19
|
};
|
|
19
20
|
|
|
@@ -21,9 +22,15 @@ const ALL_POA_TYPES: PoaDocumentType[] = [
|
|
|
21
22
|
'utility_bill',
|
|
22
23
|
'bank_statement',
|
|
23
24
|
'tenancy_agreement',
|
|
25
|
+
'government_document',
|
|
24
26
|
'other',
|
|
25
27
|
];
|
|
26
28
|
|
|
29
|
+
/** A kind THIS build can label and draw. A newer kind the dashboard offers
|
|
30
|
+
* before the SDK ships is hidden rather than rendered as a blank row. */
|
|
31
|
+
const knownKinds = (kinds: readonly string[] | undefined): PoaDocumentType[] =>
|
|
32
|
+
(kinds ?? []).filter((k): k is PoaDocumentType => (ALL_POA_TYPES as string[]).includes(k));
|
|
33
|
+
|
|
27
34
|
/** Default recency window the server checks the document date against. */
|
|
28
35
|
export const DEFAULT_POA_MAX_AGE_DAYS = 90;
|
|
29
36
|
|
|
@@ -32,10 +39,52 @@ export function hasProofOfAddressStep(poa: ProofOfAddressConfig | undefined | nu
|
|
|
32
39
|
return poa?.enabled === true;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The document kinds on offer for `country` (mirror of the web SDK's
|
|
44
|
+
* `poaOfferedKinds` — keep the three in lockstep): that country's override
|
|
45
|
+
* when the workflow declares one, else the global list, else all four.
|
|
46
|
+
*/
|
|
47
|
+
export function poaDocumentTypes(
|
|
48
|
+
poa: ProofOfAddressConfig | undefined,
|
|
49
|
+
country?: string | null,
|
|
50
|
+
): PoaDocumentType[] {
|
|
51
|
+
const override = knownKinds(country ? poa?.countryDocuments?.[country.toUpperCase()] : undefined);
|
|
52
|
+
if (override.length > 0) return override;
|
|
53
|
+
const configured = knownKinds(poa?.documentTypes);
|
|
54
|
+
return configured.length > 0 ? configured : ALL_POA_TYPES;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The name rule the server judges THIS document under — the country's per-kind
|
|
59
|
+
* exception, else the workflow default, else `required`. Mirror of the web
|
|
60
|
+
* SDK's `poaNamePolicy` and the server's `resolvePoaNamePolicy` — keep in
|
|
61
|
+
* lockstep. Read only to word the step: under `off` the header stops asking
|
|
62
|
+
* for the applicant's name.
|
|
63
|
+
*/
|
|
64
|
+
export function poaNamePolicy(
|
|
65
|
+
poa: ProofOfAddressConfig | undefined | null,
|
|
66
|
+
country: string | undefined | null,
|
|
67
|
+
kind: PoaDocumentType | undefined | null,
|
|
68
|
+
): PoaNameRule {
|
|
69
|
+
const exception = country && kind ? poa?.countryNameMatch?.[country.toUpperCase()]?.[kind] : undefined;
|
|
70
|
+
if (exception === 'required' || exception === 'optional' || exception === 'off') return exception;
|
|
71
|
+
const def = poa?.nameMatch;
|
|
72
|
+
return def === 'optional' || def === 'off' ? def : 'required';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether the org's accepted-country list admits `country`. An empty list
|
|
77
|
+
* accepts everyone; an unknown country is not refused here (the server is the
|
|
78
|
+
* gate, and it is soft on full flows).
|
|
79
|
+
*/
|
|
80
|
+
export function poaCountryAccepted(
|
|
81
|
+
poa: ProofOfAddressConfig | undefined | null,
|
|
82
|
+
country: string | undefined | null,
|
|
83
|
+
): boolean {
|
|
84
|
+
const accepted = poa?.countries;
|
|
85
|
+
if (!accepted?.length || !country) return true;
|
|
86
|
+
const code = country.toUpperCase();
|
|
87
|
+
return accepted.some((c) => c.toUpperCase() === code);
|
|
39
88
|
}
|
|
40
89
|
|
|
41
90
|
/**
|
|
@@ -63,12 +112,6 @@ export const POA_ACCEPTED_MIME_TYPES = [
|
|
|
63
112
|
'application/pdf',
|
|
64
113
|
] as const;
|
|
65
114
|
|
|
66
|
-
/**
|
|
67
|
-
* Local size cap, matching the web SDK. The server allows more, but rejecting
|
|
68
|
-
* here gives an immediate, specific message instead of a slow 413.
|
|
69
|
-
*/
|
|
70
|
-
export const POA_MAX_BYTES = 20 * 1024 * 1024;
|
|
71
|
-
|
|
72
115
|
export function isAcceptedPoaMimeType(mimeType: string | undefined): boolean {
|
|
73
116
|
const base = (mimeType?.split(';')[0] ?? '').trim().toLowerCase();
|
|
74
117
|
return (POA_ACCEPTED_MIME_TYPES as readonly string[]).includes(base);
|
package/src/config/regions.ts
CHANGED
|
@@ -116,3 +116,27 @@ export function groupCountriesByRegion(codes: string[]): RegionGroup[] {
|
|
|
116
116
|
countries: buckets.get(region)!.sort((a, b) => a.name.localeCompare(b.name)),
|
|
117
117
|
}));
|
|
118
118
|
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Lift the visitor's IP country out of its alphabetical or regional place to
|
|
122
|
+
* the top of a picker, so the one country most likely to be theirs is the
|
|
123
|
+
* first thing they see rather than something to scroll for.
|
|
124
|
+
*
|
|
125
|
+
* Takes the ALREADY-FILTERED list: the pin stays subject to the search, so
|
|
126
|
+
* typing narrows to what was asked for rather than keeping a row that does
|
|
127
|
+
* not match it. Generic over the row shape because the country-select step
|
|
128
|
+
* pins bare ISO codes and the dial-code sheet pins option objects; one rule,
|
|
129
|
+
* two callers. Mirrors the web SDK's CountryRegionPicker + PhoneNumberInput
|
|
130
|
+
* and Flutter's `pinGeoRow`.
|
|
131
|
+
*/
|
|
132
|
+
export function pinGeoRow<T>(
|
|
133
|
+
visible: readonly T[],
|
|
134
|
+
geoCountry: string | null | undefined,
|
|
135
|
+
codeOf: (item: T) => string,
|
|
136
|
+
): { pinned: T | null; rest: T[] } {
|
|
137
|
+
const geo = geoCountry?.trim().toUpperCase();
|
|
138
|
+
if (!geo) return { pinned: null, rest: [...visible] };
|
|
139
|
+
const hit = visible.find((item) => codeOf(item).toUpperCase() === geo);
|
|
140
|
+
if (hit === undefined) return { pinned: null, rest: [...visible] };
|
|
141
|
+
return { pinned: hit, rest: visible.filter((item) => item !== hit) };
|
|
142
|
+
}
|
package/src/config/stepOrder.ts
CHANGED
|
@@ -21,19 +21,37 @@
|
|
|
21
21
|
import type { WorkflowBusinessConfig } from '../types/business';
|
|
22
22
|
import type { KYCStep } from '../types/config';
|
|
23
23
|
import { applyResubmitSteps, type ResubmitConfig } from '../lib/resubmit';
|
|
24
|
+
import { addressFlowSteps, type AddressFlowOptions } from '../lib/address-flow';
|
|
24
25
|
import { businessSectionSteps, hasApplicantVerification } from './businessSteps';
|
|
25
26
|
|
|
26
27
|
export interface StepOrderOptions {
|
|
27
28
|
isBusiness: boolean;
|
|
29
|
+
/** Scoped flows: no identity section — each scope's headline step is the
|
|
30
|
+
* flow (see lib/scope.ts). */
|
|
31
|
+
scope?: import('../lib/scope').WorkflowScope | null;
|
|
28
32
|
/** Business (KYB) configuration — drives the application-section steps. */
|
|
29
33
|
business?: WorkflowBusinessConfig;
|
|
30
34
|
hasDocCapture: boolean;
|
|
31
35
|
hasNfc: boolean;
|
|
32
36
|
hasLiveness: boolean;
|
|
33
37
|
hasCountrySelect: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the flow opens on the consent screen. Absent = yes. `false` is the
|
|
40
|
+
* workflow's `consentStep: false`: the host app has already asked, so the
|
|
41
|
+
* flow opens on its first real step instead (config/consentStep.ts).
|
|
42
|
+
*/
|
|
43
|
+
hasConsent?: boolean;
|
|
34
44
|
hasEmailVerification: boolean;
|
|
35
45
|
hasPhoneVerification: boolean;
|
|
36
46
|
hasPoa: boolean;
|
|
47
|
+
hasAddressCollection: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Which address screens the flow has, from the ONE `addressFlowFor` call.
|
|
50
|
+
* The list is built by `addressFlowSteps`, the same builder the address
|
|
51
|
+
* steps navigate by, so the progress bar and the flow cannot disagree.
|
|
52
|
+
* Absent falls back to the default shape (no search, entrance offered).
|
|
53
|
+
*/
|
|
54
|
+
addressFlow?: AddressFlowOptions;
|
|
37
55
|
hasQuestionnaire: boolean;
|
|
38
56
|
/**
|
|
39
57
|
* A reviewer sent this back to redo specific steps.
|
|
@@ -68,6 +86,11 @@ export function buildStepOrder(o: StepOrderOptions): KYCStep[] {
|
|
|
68
86
|
return applyResubmitSteps(fullStepOrder(o), o.resubmit);
|
|
69
87
|
}
|
|
70
88
|
|
|
89
|
+
/** The flow's opening screen: consent, unless the workflow switched it off. */
|
|
90
|
+
function openingSteps(o: StepOrderOptions): KYCStep[] {
|
|
91
|
+
return o.hasConsent === false ? [] : ['consent'];
|
|
92
|
+
}
|
|
93
|
+
|
|
71
94
|
/** The flow as configured, before any reviewer narrowing. */
|
|
72
95
|
function fullStepOrder(o: StepOrderOptions): KYCStep[] {
|
|
73
96
|
// Business (KYB) flow — the application section, then (when the workflow
|
|
@@ -77,9 +100,9 @@ function fullStepOrder(o: StepOrderOptions): KYCStep[] {
|
|
|
77
100
|
// its questions are about the company, so it stays with the company form
|
|
78
101
|
// rather than trailing the applicant's own capture leg.
|
|
79
102
|
const steps: KYCStep[] = [
|
|
80
|
-
|
|
103
|
+
...openingSteps(o),
|
|
81
104
|
...contactSteps(o),
|
|
82
|
-
...businessSectionSteps(o.business, o.hasQuestionnaire),
|
|
105
|
+
...businessSectionSteps(o.business, o.hasQuestionnaire, o.hasAddressCollection),
|
|
83
106
|
];
|
|
84
107
|
if (hasApplicantVerification(o.business)) {
|
|
85
108
|
// The applicant may hold an ID issued anywhere the org can verify —
|
|
@@ -93,11 +116,52 @@ function fullStepOrder(o: StepOrderOptions): KYCStep[] {
|
|
|
93
116
|
return steps;
|
|
94
117
|
}
|
|
95
118
|
|
|
119
|
+
// Scoped flows: the scope's headline section IS the flow.
|
|
120
|
+
if (o.scope === 'address') {
|
|
121
|
+
const steps: KYCStep[] = [...openingSteps(o), ...contactSteps(o)];
|
|
122
|
+
if (o.hasPoa) steps.push('proof-of-address');
|
|
123
|
+
steps.push(
|
|
124
|
+
...addressFlowSteps(
|
|
125
|
+
o.addressFlow ?? { searchAvailable: false, photoMode: 'optional', streetViewOffered: false },
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
if (o.hasQuestionnaire) steps.push('questionnaire');
|
|
129
|
+
steps.push('submitted');
|
|
130
|
+
return steps;
|
|
131
|
+
}
|
|
132
|
+
if (o.scope === 'biometric-authentication' || o.scope === 'biometric-enrollment') {
|
|
133
|
+
const steps: KYCStep[] = [...openingSteps(o), ...contactSteps(o), 'liveness'];
|
|
134
|
+
if (o.hasQuestionnaire) steps.push('questionnaire');
|
|
135
|
+
steps.push('submitted');
|
|
136
|
+
return steps;
|
|
137
|
+
}
|
|
138
|
+
if (o.scope === 'questionnaire') {
|
|
139
|
+
return [...openingSteps(o), ...contactSteps(o), 'questionnaire', 'submitted'];
|
|
140
|
+
}
|
|
141
|
+
if (o.scope === 'contact') {
|
|
142
|
+
return [...openingSteps(o), ...contactSteps(o), 'submitted'];
|
|
143
|
+
}
|
|
144
|
+
|
|
96
145
|
const middle: KYCStep[] = [...captureLeg(o)];
|
|
97
146
|
if (o.hasPoa) middle.push('proof-of-address');
|
|
147
|
+
// The address flow is FOUR real steps on an individual flow (find it, confirm
|
|
148
|
+
// it, show it, commit it), so the progress bar advances through them and back
|
|
149
|
+
// is ordinary step navigation. KYB keeps the single premises step, inserted
|
|
150
|
+
// by businessSectionSteps above.
|
|
151
|
+
if (o.hasAddressCollection) {
|
|
152
|
+
middle.push(
|
|
153
|
+
...addressFlowSteps(
|
|
154
|
+
o.addressFlow ?? {
|
|
155
|
+
searchAvailable: false,
|
|
156
|
+
photoMode: 'optional',
|
|
157
|
+
streetViewOffered: false,
|
|
158
|
+
},
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
98
162
|
if (o.hasQuestionnaire) middle.push('questionnaire');
|
|
99
163
|
return [
|
|
100
|
-
|
|
164
|
+
...openingSteps(o),
|
|
101
165
|
...contactSteps(o),
|
|
102
166
|
...(o.hasCountrySelect ? (['country-select'] as KYCStep[]) : []),
|
|
103
167
|
'id-type',
|
|
@@ -129,10 +193,20 @@ export function nextStepInOrder(step: KYCStep, o: StepOrderOptions): KYCStep {
|
|
|
129
193
|
return order[index + 1] ?? order[order.length - 1]!;
|
|
130
194
|
}
|
|
131
195
|
|
|
132
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* The step before `step` — what the back button goes to.
|
|
198
|
+
*
|
|
199
|
+
* A step not in the order stands still, for the same reason `nextStepInOrder`
|
|
200
|
+
* refuses to guess at one: it has no predecessor in a flow that does not
|
|
201
|
+
* contain it. This used to fall through to `order[0]`, so an applicant resumed
|
|
202
|
+
* onto a step the flow had since dropped was thrown all the way back to consent
|
|
203
|
+
* by a single back-press, discarding everything they had done. Standing still
|
|
204
|
+
* is recoverable; that was not.
|
|
205
|
+
*/
|
|
133
206
|
export function previousStepInOrder(step: KYCStep, o: StepOrderOptions): KYCStep {
|
|
134
207
|
const order = buildStepOrder(o);
|
|
135
208
|
const index = order.indexOf(step);
|
|
136
|
-
if (index
|
|
209
|
+
if (index < 0) return step;
|
|
210
|
+
if (index === 0) return order[0]!;
|
|
137
211
|
return order[index - 1]!;
|
|
138
212
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Upload size limits.
|
|
3
|
+
//
|
|
4
|
+
// One rule for every document the applicant attaches (proof of address, the
|
|
5
|
+
// KYB company documents, the entrance photo): images up to 5 MB, PDFs up to
|
|
6
|
+
// 15 MB (user decision 2026-09-06). The server's own cap is higher, so refusing
|
|
7
|
+
// here gives an immediate, specific message instead of a slow 413. A THREE-WAY
|
|
8
|
+
// MIRROR with the web SDK's `lib/upload-limits.ts` and Flutter's
|
|
9
|
+
// `config/upload_limits.dart`, pinned to
|
|
10
|
+
// `kyc-sdk-flutter/test/upload_limits_vectors.json`.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export const IMAGE_MAX_BYTES = 5 * 1024 * 1024;
|
|
14
|
+
export const PDF_MAX_BYTES = 15 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
/** The line under every drop zone: what is accepted, and how big. */
|
|
17
|
+
export const UPLOAD_HINT = 'PDF, JPG, PNG · PDF max 15 MB, images max 5 MB';
|
|
18
|
+
|
|
19
|
+
export function isPdfMime(mime: string | null | undefined): boolean {
|
|
20
|
+
return (mime?.split(';')[0] ?? '').trim().toLowerCase() === 'application/pdf';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The refusal for a file over its cap, or null when it fits. An unknown size
|
|
25
|
+
* also passes: the server still judges the bytes at its own cap, and a picker
|
|
26
|
+
* that reports no size must not block a good file.
|
|
27
|
+
*/
|
|
28
|
+
export function uploadSizeError(
|
|
29
|
+
mime: string | null | undefined,
|
|
30
|
+
size: number | null | undefined,
|
|
31
|
+
): string | null {
|
|
32
|
+
if (typeof size !== 'number' || !Number.isFinite(size)) return null;
|
|
33
|
+
if (isPdfMime(mime)) return size > PDF_MAX_BYTES ? 'PDF is too large (max 15 MB).' : null;
|
|
34
|
+
return size > IMAGE_MAX_BYTES ? 'Image is too large (max 5 MB).' : null;
|
|
35
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export const WORKFLOW_KEYS = [
|
|
18
18
|
'subjectType',
|
|
19
|
+
'scope',
|
|
19
20
|
'business',
|
|
20
21
|
'country',
|
|
21
22
|
'countries',
|
|
@@ -27,8 +28,10 @@ export const WORKFLOW_KEYS = [
|
|
|
27
28
|
'enableLiveness',
|
|
28
29
|
'livenessMode',
|
|
29
30
|
'flashSequenceLength',
|
|
31
|
+
'biometric',
|
|
30
32
|
'deviceIntelligence',
|
|
31
33
|
'keyPeopleLinkRecovery',
|
|
34
|
+
'consentStep',
|
|
32
35
|
'requireMobileDevice',
|
|
33
36
|
'voiceGuidance',
|
|
34
37
|
'showThemeToggle',
|
|
@@ -41,6 +44,7 @@ export const WORKFLOW_KEYS = [
|
|
|
41
44
|
'phoneVerification',
|
|
42
45
|
'questionnaire',
|
|
43
46
|
'proofOfAddress',
|
|
47
|
+
'addressCollection',
|
|
44
48
|
'nfc',
|
|
45
49
|
// Set only on a session a reviewer sent back, never on a published flow.
|
|
46
50
|
'resubmit',
|
|
@@ -53,8 +57,10 @@ export type WorkflowKey = (typeof WORKFLOW_KEYS)[number];
|
|
|
53
57
|
* key it DEFINES; an absent key leaves the prop alone (which is how a flow that
|
|
54
58
|
* only sets, say, `livenessMode` doesn't wipe a prop-supplied `userId`).
|
|
55
59
|
*
|
|
56
|
-
* `appearance`
|
|
57
|
-
* that only sets `primaryColor` doesn't wipe a
|
|
60
|
+
* `appearance` and `biometric` merge shallowly with flow keys winning
|
|
61
|
+
* per-field, so a flow that only sets `primaryColor` doesn't wipe a
|
|
62
|
+
* prop-supplied `logo`, and a flow that only switches the selfie review on
|
|
63
|
+
* doesn't wipe a host app's `doneButton: false`.
|
|
58
64
|
*
|
|
59
65
|
* Pure and side-effect free.
|
|
60
66
|
*/
|
|
@@ -67,10 +73,10 @@ export function mergeWorkflowConfig<P extends Record<string, unknown>>(
|
|
|
67
73
|
for (const key of WORKFLOW_KEYS) {
|
|
68
74
|
const value = flowConfig[key];
|
|
69
75
|
if (value === undefined) continue;
|
|
70
|
-
if (key === 'appearance') {
|
|
71
|
-
const
|
|
76
|
+
if (key === 'appearance' || key === 'biometric') {
|
|
77
|
+
const propBlock = props[key];
|
|
72
78
|
merged[key] = {
|
|
73
|
-
...(typeof
|
|
79
|
+
...(typeof propBlock === 'object' && propBlock !== null ? propBlock : {}),
|
|
74
80
|
...(value as Record<string, unknown>),
|
|
75
81
|
};
|
|
76
82
|
} else {
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
export { MyazaKYC, useMyazaKYC } from './MyazaKYC';
|
|
12
12
|
export type { MyazaKYCProps, UseMyazaKYCReturn } from './MyazaKYC';
|
|
13
13
|
|
|
14
|
+
// Returning-user face re-authentication (the web SDK's MyazaBiometricAuth).
|
|
15
|
+
export { MyazaBiometricAuth } from './MyazaBiometricAuth';
|
|
16
|
+
export type { MyazaBiometricAuthProps } from './MyazaBiometricAuth';
|
|
17
|
+
export type {
|
|
18
|
+
BiometricAuthRequest,
|
|
19
|
+
BiometricAuthResponse,
|
|
20
|
+
BiometricStatusResponse,
|
|
21
|
+
BiometricLivenessClaim,
|
|
22
|
+
} from './services/api-types-biometric';
|
|
23
|
+
|
|
14
24
|
// Public config + callback types
|
|
15
25
|
export type {
|
|
16
26
|
MyazaKYCConfig,
|
|
@@ -32,6 +42,7 @@ export type {
|
|
|
32
42
|
PhoneVerificationConfig,
|
|
33
43
|
ProofOfAddressConfig,
|
|
34
44
|
PoaDocumentType,
|
|
45
|
+
AddressCollectionConfig,
|
|
35
46
|
QuestionnaireConfig,
|
|
36
47
|
QuestionnaireField,
|
|
37
48
|
QuestionnaireFieldOption,
|
|
@@ -56,7 +67,8 @@ export type {
|
|
|
56
67
|
WorkflowBusinessDocumentTypeConfig,
|
|
57
68
|
WorkflowBusinessApplicantConfig,
|
|
58
69
|
} from './types/business';
|
|
59
|
-
export type { KYCSubmission, KYCErrorCode, KYCErrorDetails } from './types/verification';
|
|
70
|
+
export type { KYCSubmission, KYCResult, KYCErrorCode, KYCErrorDetails } from './types/verification';
|
|
71
|
+
export type { BiometricFlowConfig, BiometricCopy, BiometricCopyText } from './config/biometricOptions';
|
|
60
72
|
export { KYCError } from './types/verification';
|
|
61
73
|
|
|
62
74
|
// ID-type matrix + helpers
|
|
@@ -107,6 +119,47 @@ export {
|
|
|
107
119
|
export { hasActiveQuestionnaire, questionnaireAnswerKeys } from './config/questionnaire';
|
|
108
120
|
export { hasEmailVerificationStep, hasPhoneVerificationStep } from './config/contact';
|
|
109
121
|
export { hasProofOfAddressStep, poaDocumentTypes, poaTypeLabel } from './config/proofOfAddress';
|
|
122
|
+
export { hasAddressCollectionStep } from './config/addressCollection';
|
|
123
|
+
// Presence verification (Address Intelligence Phase 2): the foreground
|
|
124
|
+
// reporter the HOST APP calls on app open, plus the on-device pin store.
|
|
125
|
+
export {
|
|
126
|
+
reportAddressPresence,
|
|
127
|
+
type ReportPresenceOptions,
|
|
128
|
+
type ReportPresenceResult,
|
|
129
|
+
} from './presence/report';
|
|
130
|
+
// The background (OS geofence) tier — the always-on model. register* must run
|
|
131
|
+
// at app-root module scope; enable* asks for the background permission.
|
|
132
|
+
export {
|
|
133
|
+
PRESENCE_GEOFENCE_TASK,
|
|
134
|
+
disableBackgroundPresence,
|
|
135
|
+
enableBackgroundPresence,
|
|
136
|
+
registerBackgroundPresence,
|
|
137
|
+
type EnableBackgroundResult,
|
|
138
|
+
} from './presence/background';
|
|
139
|
+
// The Android foreground-service tier (the OkHi reliability move): a
|
|
140
|
+
// persistent notification keeps the process alive on phones whose battery
|
|
141
|
+
// managers drop geofence transitions. Opt-in; the host words the notification.
|
|
142
|
+
export {
|
|
143
|
+
PRESENCE_LOCATION_TASK,
|
|
144
|
+
disableForegroundService,
|
|
145
|
+
enableForegroundService,
|
|
146
|
+
foregroundServiceRunning,
|
|
147
|
+
type EnableForegroundServiceResult,
|
|
148
|
+
type PresenceNotification,
|
|
149
|
+
} from './presence/foreground-service';
|
|
150
|
+
export {
|
|
151
|
+
openLocationSettings,
|
|
152
|
+
presenceStatus,
|
|
153
|
+
type PresenceStatus,
|
|
154
|
+
type PresenceTier,
|
|
155
|
+
} from './presence/status';
|
|
156
|
+
export { resolvePresenceTier, type PermissionState, type TierInputs } from './presence/tier';
|
|
157
|
+
// savePresencePin is the org-side handoff for STANDALONE address verification:
|
|
158
|
+
// when capture happened on a hosted web link (or the org's own backend already
|
|
159
|
+
// holds the address), the host app hands the SDK the pin so the foreground and
|
|
160
|
+
// background presence tiers can run — the pin otherwise only ever exists where
|
|
161
|
+
// OUR capture step stored it.
|
|
162
|
+
export { clearPresencePin, savePresencePin } from './presence/store';
|
|
110
163
|
|
|
111
164
|
// Country grouping for multi-region flows
|
|
112
165
|
export { groupCountriesByRegion, regionCountryName, type Region } from './config/regions';
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { AddressParts } from '../services/api';
|
|
2
|
+
import type { LocationFailure } from '../services/location';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// The device's current location, fetched ONCE per verification and shared by
|
|
6
|
+
// every address step.
|
|
7
|
+
//
|
|
8
|
+
// Module-level on purpose: the steps mount and unmount as the person walks the
|
|
9
|
+
// flow, and re-prompting or re-fixing on every screen is exactly the hesitation
|
|
10
|
+
// this exists to remove. Every address step starts the prefetch on mount, so
|
|
11
|
+
// the GPS warms up while the person is still reading the SEARCH screen; by the
|
|
12
|
+
// pin step the fix — and its reverse-geocoded line — are usually already in
|
|
13
|
+
// hand, and the map lands right first time instead of showing a default view
|
|
14
|
+
// and then jumping.
|
|
15
|
+
//
|
|
16
|
+
// A MIRROR of the web SDK's steps/address/current-location.ts, with one
|
|
17
|
+
// deliberate difference: web scopes the cache to a PAGE session, which ends at
|
|
18
|
+
// the next reload. An app session spans many opens of the SDK, so a fix cached
|
|
19
|
+
// an hour ago would drop the pin at yesterday's address. Store CREATION clears
|
|
20
|
+
// it (one store per modal launch), making "this verification" the mobile
|
|
21
|
+
// equivalent of that scope. Clearing it only from the store's reset() is not
|
|
22
|
+
// enough: reset() is a consumer-facing restart, not part of the open path.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export interface CurrentFix {
|
|
26
|
+
lat: number;
|
|
27
|
+
lng: number;
|
|
28
|
+
accuracy: number | null;
|
|
29
|
+
/** The reverse-geocoded line ("11 Bassey Street, Calabar"), when known. */
|
|
30
|
+
label: string | null;
|
|
31
|
+
parts: AddressParts | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A GPS read: the fix, or WHY there is none (services/location.ts). */
|
|
35
|
+
export type FixOutcome =
|
|
36
|
+
| { fix: { lat: number; lng: number; accuracy: number | null } }
|
|
37
|
+
| { failure: LocationFailure };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What the cache needs to produce a fix.
|
|
41
|
+
*
|
|
42
|
+
* The GPS reader is INJECTED rather than imported: the store's reset clears
|
|
43
|
+
* this cache, and importing expo-location here would drag a native module into
|
|
44
|
+
* the store's graph for the sake of resetting three variables.
|
|
45
|
+
*/
|
|
46
|
+
export interface FixSource {
|
|
47
|
+
takeFix(): Promise<FixOutcome>;
|
|
48
|
+
addressReverse(
|
|
49
|
+
lat: number,
|
|
50
|
+
lng: number,
|
|
51
|
+
): Promise<{ line: string | null; parts?: AddressParts | null }>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let resolved: CurrentFix | null = null;
|
|
55
|
+
let inflight: Promise<CurrentFix | null> | null = null;
|
|
56
|
+
let failed = false;
|
|
57
|
+
let lastFailure: LocationFailure | null = null;
|
|
58
|
+
|
|
59
|
+
/** Why the last attempt failed, so the message can say something true. */
|
|
60
|
+
export function currentFixFailure(): LocationFailure | null {
|
|
61
|
+
return lastFailure;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The message for a failed read. A refused permission, a phone that cannot
|
|
66
|
+
* place itself, and a fix that took too long are three different problems
|
|
67
|
+
* with three different remedies, and one line telling everybody to "allow
|
|
68
|
+
* location access" sent people to a permission that was already granted.
|
|
69
|
+
* Placing the pin by hand always works, so every line says so. Mirrors the
|
|
70
|
+
* web SDK's locationFailureMessage and Flutter's.
|
|
71
|
+
*/
|
|
72
|
+
export function locationFailureMessage(reason: LocationFailure | null): string {
|
|
73
|
+
switch (reason) {
|
|
74
|
+
case 'denied':
|
|
75
|
+
return "Location access is blocked for this app. Allow it in your phone's Settings, then try again, or place the pin yourself.";
|
|
76
|
+
case 'unavailable':
|
|
77
|
+
return 'Your phone could not work out where it is right now. Check that location is switched on, then try again, or place the pin yourself.';
|
|
78
|
+
case 'timeout':
|
|
79
|
+
return 'Finding your location took too long. Try again, or place the pin yourself.';
|
|
80
|
+
default:
|
|
81
|
+
return 'This device cannot share its location. Place the pin yourself.';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The fix, when one has already resolved for this verification. */
|
|
86
|
+
export function currentFix(): CurrentFix | null {
|
|
87
|
+
return resolved;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Is a fix attempt still running? */
|
|
91
|
+
export function locating(): boolean {
|
|
92
|
+
return inflight !== null && resolved === null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Start (or JOIN) the one location attempt. Safe to call from every address
|
|
97
|
+
* step's mount: the OS permission prompt fires at most once, and a second
|
|
98
|
+
* caller waits on the first attempt rather than starting its own.
|
|
99
|
+
*
|
|
100
|
+
* Resolves null on a denied or failed read — callers fall back to the manual
|
|
101
|
+
* pin, which is what every address failure degrades to.
|
|
102
|
+
*/
|
|
103
|
+
export function prefetchCurrentFix(
|
|
104
|
+
source: FixSource,
|
|
105
|
+
opts?: { retry?: boolean },
|
|
106
|
+
): Promise<CurrentFix | null> {
|
|
107
|
+
if (resolved) return Promise.resolve(resolved);
|
|
108
|
+
// A failed attempt (denied prompt, no fix) is never retried AUTOMATICALLY.
|
|
109
|
+
// Every step mount calls this, so re-arming on failure left the location row
|
|
110
|
+
// spinning forever and re-fired the OS prompt on each screen. An explicit
|
|
111
|
+
// TAP passes `retry` and gets a fresh attempt, since the person may have
|
|
112
|
+
// granted permission in the meantime.
|
|
113
|
+
if (failed && !inflight && !opts?.retry) return Promise.resolve(null);
|
|
114
|
+
if (!inflight) {
|
|
115
|
+
inflight = (async () => {
|
|
116
|
+
// No builder-preview branch: the preview is a web-only surface, and a
|
|
117
|
+
// canned fix here would be a code path no mobile caller can reach.
|
|
118
|
+
const outcome = await source.takeFix().catch((): FixOutcome => ({ failure: 'unsupported' }));
|
|
119
|
+
if (!('fix' in outcome)) {
|
|
120
|
+
inflight = null;
|
|
121
|
+
failed = true;
|
|
122
|
+
lastFailure = outcome.failure;
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const fix = outcome.fix;
|
|
126
|
+
lastFailure = null;
|
|
127
|
+
let label: string | null = null;
|
|
128
|
+
let parts: AddressParts | null = null;
|
|
129
|
+
try {
|
|
130
|
+
const r = await source.addressReverse(fix.lat, fix.lng);
|
|
131
|
+
label = r.line ?? null;
|
|
132
|
+
parts = r.parts ?? null;
|
|
133
|
+
} catch {
|
|
134
|
+
/* the coordinates alone are still a fix */
|
|
135
|
+
}
|
|
136
|
+
resolved = { lat: fix.lat, lng: fix.lng, accuracy: fix.accuracy, label, parts };
|
|
137
|
+
failed = false;
|
|
138
|
+
return resolved;
|
|
139
|
+
})();
|
|
140
|
+
}
|
|
141
|
+
return inflight;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Forget the fix so the next verification takes its own. Called from the
|
|
145
|
+
* store's reset, beside the step log. */
|
|
146
|
+
export function resetCurrentFix(): void {
|
|
147
|
+
resolved = null;
|
|
148
|
+
inflight = null;
|
|
149
|
+
failed = false;
|
|
150
|
+
lastFailure = null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── The pin step's automatic first locate ───────────────────────────────────
|
|
154
|
+
//
|
|
155
|
+
// Most people are verifying from home, so the map should land on them rather
|
|
156
|
+
// than a country-centre default. It fires ONCE: a dismissed or denied prompt
|
|
157
|
+
// must not re-fire every time the person passes back through the pin step, and
|
|
158
|
+
// the explicit control is there for retries.
|
|
159
|
+
|
|
160
|
+
let autoLocateAttempted = false;
|
|
161
|
+
|
|
162
|
+
/** True the FIRST time it is called for a verification, false after. */
|
|
163
|
+
export function claimAutoLocate(): boolean {
|
|
164
|
+
if (autoLocateAttempted) return false;
|
|
165
|
+
autoLocateAttempted = true;
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function resetAutoLocate(): void {
|
|
170
|
+
autoLocateAttempted = false;
|
|
171
|
+
}
|