@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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius, spacing } from '../../config/theme';
|
|
5
|
+
import { useKyc, useKycConfig, useKycStore, useTheme } from '../../components/runtime';
|
|
6
|
+
import { MyazaText } from '../../components/Typography';
|
|
7
|
+
import { MyazaButton } from '../../components/MyazaButton';
|
|
8
|
+
import { Icon } from '../../components/Icon';
|
|
9
|
+
import { MilestoneTrack, type Milestone } from '../../components/MilestoneTrack';
|
|
10
|
+
import { IntroDisclosures } from './IntroDisclosures';
|
|
11
|
+
import type { KYCStep } from '../../types/config';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// The presence "how it works" screen, shown ONCE before the address flow's
|
|
15
|
+
// first step. Drawn in the SUCCESS CARD's language — eyebrow pill, header band,
|
|
16
|
+
// milestone track — so the promise made here and the "address check active"
|
|
17
|
+
// card at the end read as two frames of one story.
|
|
18
|
+
//
|
|
19
|
+
// The underlying step still does its mount work behind this gate: the
|
|
20
|
+
// current-fix prefetch starts while the primer is on screen, so by the time
|
|
21
|
+
// "Got it" is tapped the fix is usually already in hand and the pin lands with
|
|
22
|
+
// no hesitation. Only the APPLY waits.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
// The second milestone's caption depends on which tier the org runs. With
|
|
26
|
+
// background geofencing on, the "allow all the time" prompt is coming, and
|
|
27
|
+
// OkHi's integration guidance is to say so ONCE, up front, beside the
|
|
28
|
+
// education — not to surprise the person with it after capture.
|
|
29
|
+
const CHECK_IN_CAPTION = {
|
|
30
|
+
foreground: 'Keep location on; your phone confirms it over the coming days.',
|
|
31
|
+
background:
|
|
32
|
+
'Allow location all the time when asked. Your phone then confirms it on its own, even with the app closed.',
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
const milestonesFor = (background: boolean): readonly Milestone[] => [
|
|
36
|
+
{
|
|
37
|
+
icon: 'map-pin-house',
|
|
38
|
+
stage: 'Your part',
|
|
39
|
+
title: 'Pin your address',
|
|
40
|
+
caption: 'Put the pin right on your building. Takes a minute.',
|
|
41
|
+
state: 'active',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
icon: 'radar',
|
|
45
|
+
stage: 'After that',
|
|
46
|
+
title: 'Quiet check-ins',
|
|
47
|
+
caption: background ? CHECK_IN_CAPTION.background : CHECK_IN_CAPTION.foreground,
|
|
48
|
+
state: 'ahead',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
icon: 'bell-ring',
|
|
52
|
+
stage: 'Then',
|
|
53
|
+
title: 'Confirmed',
|
|
54
|
+
caption: 'You’ll be notified. That is it.',
|
|
55
|
+
state: 'ahead',
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The gate for `step`, or null when it should not show. Returning the element
|
|
61
|
+
* (rather than a boolean) keeps the decision and the screen in one place: a
|
|
62
|
+
* step renders `gate ?? its own body`.
|
|
63
|
+
*/
|
|
64
|
+
export function useAddressIntroGate(
|
|
65
|
+
step: KYCStep,
|
|
66
|
+
firstStep: KYCStep,
|
|
67
|
+
): React.ReactElement | null {
|
|
68
|
+
const config = useKycConfig();
|
|
69
|
+
const store = useKycStore();
|
|
70
|
+
const { colors } = useTheme();
|
|
71
|
+
const seen = useKyc((s) => s.addressIntroSeen);
|
|
72
|
+
const presence = config.addressCollection?.presence?.enabled === true;
|
|
73
|
+
const background = config.addressCollection?.presence?.background === true;
|
|
74
|
+
|
|
75
|
+
if (step !== firstStep || !presence || seen) return null;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<View>
|
|
79
|
+
{/* Web's exact tints (border-primary/15, bg-primary/[0.06], the badge
|
|
80
|
+
on primary/10), as hex alpha on the primary so a custom brand and
|
|
81
|
+
dark mode both keep the relationship; Flutter draws the same. */}
|
|
82
|
+
<View
|
|
83
|
+
style={{
|
|
84
|
+
borderWidth: 1,
|
|
85
|
+
borderColor: `${colors.primary}26`,
|
|
86
|
+
borderRadius: radius.md,
|
|
87
|
+
overflow: 'hidden',
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<View
|
|
91
|
+
style={{
|
|
92
|
+
backgroundColor: `${colors.primary}0F`,
|
|
93
|
+
paddingHorizontal: spacing.md,
|
|
94
|
+
paddingTop: spacing.md,
|
|
95
|
+
paddingBottom: spacing.sm + 4,
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
<View
|
|
99
|
+
style={{
|
|
100
|
+
flexDirection: 'row',
|
|
101
|
+
alignItems: 'center',
|
|
102
|
+
alignSelf: 'flex-start',
|
|
103
|
+
gap: spacing.sm,
|
|
104
|
+
paddingHorizontal: spacing.sm + 2,
|
|
105
|
+
paddingVertical: 4,
|
|
106
|
+
borderRadius: radius.full,
|
|
107
|
+
backgroundColor: `${colors.primary}1A`,
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
<Icon name="map-pin-check" size={12} color={colors.primary} />
|
|
111
|
+
<MyazaText
|
|
112
|
+
variant="bodySmall"
|
|
113
|
+
color={colors.primary}
|
|
114
|
+
style={{
|
|
115
|
+
fontSize: 11,
|
|
116
|
+
fontWeight: '600',
|
|
117
|
+
letterSpacing: 0.6,
|
|
118
|
+
textTransform: 'uppercase',
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
Address verification
|
|
122
|
+
</MyazaText>
|
|
123
|
+
</View>
|
|
124
|
+
{/* Web's `text-base font-semibold`: the BODY face at 16/600, not a
|
|
125
|
+
heading, so it matches the review card's address line. */}
|
|
126
|
+
<MyazaText variant="body" style={{ marginTop: spacing.sm, fontWeight: '600' }}>
|
|
127
|
+
Let’s confirm your address
|
|
128
|
+
</MyazaText>
|
|
129
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: 2 }}>
|
|
130
|
+
This address will be verified over the coming days. Your part takes a minute; the rest
|
|
131
|
+
happens on its own.
|
|
132
|
+
</MyazaText>
|
|
133
|
+
</View>
|
|
134
|
+
|
|
135
|
+
<MilestoneTrack milestones={milestonesFor(background)} numbered />
|
|
136
|
+
</View>
|
|
137
|
+
|
|
138
|
+
<View style={{ height: spacing.md }} />
|
|
139
|
+
<IntroDisclosures background={background} />
|
|
140
|
+
|
|
141
|
+
<View style={{ height: spacing.lg }} />
|
|
142
|
+
<MyazaButton
|
|
143
|
+
label="Got it, let’s go"
|
|
144
|
+
onPress={() => store.getState().markAddressIntroSeen()}
|
|
145
|
+
/>
|
|
146
|
+
</View>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { MyazaText } from '../../components/Typography';
|
|
5
|
+
import { Icon } from '../../components/Icon';
|
|
6
|
+
import { DashedBorder } from '../../components/DashedBorder';
|
|
7
|
+
import { useTheme } from '../../components/runtime';
|
|
8
|
+
import { spacing } from '../../config/theme';
|
|
9
|
+
import type { LatLng } from '../../lib/map-tiles';
|
|
10
|
+
|
|
11
|
+
// The SANDBOX map stand-in (the web SDK's preview placeholder, for test
|
|
12
|
+
// keys): no tiles, no framed page, no vendor loads. The pin lands on the
|
|
13
|
+
// default centre once — what the real map's first idle emits — so Continue
|
|
14
|
+
// stays reachable and the integrator walks the whole flow.
|
|
15
|
+
export function AddressMapStub({
|
|
16
|
+
hasPin,
|
|
17
|
+
onLand,
|
|
18
|
+
defaultCenter,
|
|
19
|
+
height,
|
|
20
|
+
}: {
|
|
21
|
+
hasPin: boolean;
|
|
22
|
+
onLand: (pin: LatLng) => void;
|
|
23
|
+
defaultCenter: LatLng;
|
|
24
|
+
height: number;
|
|
25
|
+
}) {
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!hasPin) onLand(defaultCenter);
|
|
28
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
29
|
+
}, [hasPin]);
|
|
30
|
+
|
|
31
|
+
const { colors } = useTheme();
|
|
32
|
+
return (
|
|
33
|
+
<View
|
|
34
|
+
style={{
|
|
35
|
+
height,
|
|
36
|
+
borderRadius: 12,
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
justifyContent: 'center',
|
|
39
|
+
padding: spacing.lg,
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<DashedBorder color={colors.border} radius={12} strokeWidth={1} />
|
|
43
|
+
<Icon name="map-pin-house" size={32} color={colors.textSecondary} />
|
|
44
|
+
<View style={{ height: spacing.sm }} />
|
|
45
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary} style={{ textAlign: 'center' }}>
|
|
46
|
+
Applicants place their pin on a live map here. The map loads only for real users.
|
|
47
|
+
</MyazaText>
|
|
48
|
+
</View>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { useWindowDimensions, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../../config/theme';
|
|
5
|
+
import { useKyc, useKycStore, useTheme } from '../../components/runtime';
|
|
6
|
+
import { MyazaText } from '../../components/Typography';
|
|
7
|
+
import { MyazaButton } from '../../components/MyazaButton';
|
|
8
|
+
import { FramedMapPicker } from '../../components/FramedMapPicker';
|
|
9
|
+
import { StickyActions } from '../../components/StickyActions';
|
|
10
|
+
import { displayAddressLine, shouldAskLabelDecision, addressVendorsStubbed } from '../../lib/address-flow';
|
|
11
|
+
import { missingFieldsNudge, missingRequiredAddressFields } from '../../lib/address-field-modes';
|
|
12
|
+
import { claimAutoLocate } from '../../lib/address-current-location';
|
|
13
|
+
import { mapSurfaceHeight } from '../../lib/map-tiles';
|
|
14
|
+
import { useAddressFlow } from './use-address-flow';
|
|
15
|
+
import { AddressMapStub } from './AddressMapStub';
|
|
16
|
+
import { useAddressIntroGate } from './AddressIntroGate';
|
|
17
|
+
import { CurrentLocationRow, LocateChip } from './CurrentLocationRow';
|
|
18
|
+
import { LabelDecisionRow } from './LabelDecisionRow';
|
|
19
|
+
import { DetailsSheet } from './DetailsSheet';
|
|
20
|
+
import { detailValuesOf } from './detail-values';
|
|
21
|
+
import { PinSummaryRow } from './PinSummaryRow';
|
|
22
|
+
import { SkipForNow } from './SkipForNow';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The PIN step (wire name 'address-collection', kept so older session progress
|
|
26
|
+
* restores cleanly and the server's step-log titles stay meaningful).
|
|
27
|
+
*
|
|
28
|
+
* A big map — sized by mapSurfaceHeight, so a phone keeps room to scroll —
|
|
29
|
+
* with the summary row and the details sheet beneath it, and Continue held at
|
|
30
|
+
* the bottom edge (StickyActions) so the map can never put it out of reach.
|
|
31
|
+
* On KYB this is the WHOLE premises capture, so Continue commits (attest fix
|
|
32
|
+
* included); on an individual flow it advances to the entrance and review
|
|
33
|
+
* steps. Workflow-required details hold Continue and open the sheet on the
|
|
34
|
+
* missing fields rather than pointing at a closed drawer.
|
|
35
|
+
*/
|
|
36
|
+
export function AddressPinStep(): React.ReactElement {
|
|
37
|
+
const store = useKycStore();
|
|
38
|
+
const { colors } = useTheme();
|
|
39
|
+
const viewport = useWindowDimensions();
|
|
40
|
+
const flow = useAddressFlow();
|
|
41
|
+
// The framed Google picker when the platform serves one; the OSM picker
|
|
42
|
+
// otherwise (and whenever the page never says ready).
|
|
43
|
+
const mapsFrameUrl = useKyc((s) => s.serverConfig.mapsFrameUrl ?? null);
|
|
44
|
+
// SANDBOX renders the stand-in (the web SDK's vendor-stub rule): verdicts
|
|
45
|
+
// are canned server-side, so a live map on a test key only spends quota.
|
|
46
|
+
const vendorsStubbed = addressVendorsStubbed({
|
|
47
|
+
environment: useKyc((s) => s.serverConfig.environment ?? null),
|
|
48
|
+
});
|
|
49
|
+
const [sheetOpen, setSheetOpen] = useState(false);
|
|
50
|
+
const [missingNudge, setMissingNudge] = useState(false);
|
|
51
|
+
const gate = useAddressIntroGate('address-collection', flow.steps[0]!);
|
|
52
|
+
|
|
53
|
+
const gateShowing = Boolean(gate);
|
|
54
|
+
const noPin = !flow.pin;
|
|
55
|
+
const { startPrefetch, relabelPin, applyCurrentFix } = flow;
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
// The fix warms even under the primer: only the APPLY waits for it, since
|
|
58
|
+
// a pin landing behind the gate would be invisible anyway.
|
|
59
|
+
startPrefetch();
|
|
60
|
+
if (gateShowing) return;
|
|
61
|
+
// A restored pin without its line (a snapshot saved before labels existed):
|
|
62
|
+
// reverse-geocode it once rather than showing raw coordinates.
|
|
63
|
+
relabelPin();
|
|
64
|
+
// Most people are verifying from home, so the map should land on them
|
|
65
|
+
// rather than a country-centre default. Silent by contract, and claimed
|
|
66
|
+
// once per verification: a dismissed or denied prompt must not re-fire
|
|
67
|
+
// every time the person passes back through this step.
|
|
68
|
+
if (!noPin || !claimAutoLocate()) return;
|
|
69
|
+
void applyCurrentFix(undefined, { silent: true });
|
|
70
|
+
}, [gateShowing, noPin, startPrefetch, relabelPin, applyCurrentFix]);
|
|
71
|
+
|
|
72
|
+
if (gate) return gate;
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
const address = flow.address;
|
|
77
|
+
const label = address?.label ?? null;
|
|
78
|
+
const askLabel = address ? shouldAskLabelDecision(address) : false;
|
|
79
|
+
// The pin step is the last of the flow on KYB, so its Continue commits.
|
|
80
|
+
const lastInFlow = flow.isBusiness;
|
|
81
|
+
const mapHeight = mapSurfaceHeight(viewport);
|
|
82
|
+
|
|
83
|
+
const missingRequired = missingRequiredAddressFields(flow.cfg, address);
|
|
84
|
+
const handleContinue = () => {
|
|
85
|
+
if (missingRequired.length > 0) {
|
|
86
|
+
setMissingNudge(true);
|
|
87
|
+
setSheetOpen(true);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (lastInFlow) void flow.confirm();
|
|
91
|
+
else flow.goNext();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<>
|
|
96
|
+
<StickyActions
|
|
97
|
+
actions={
|
|
98
|
+
<>
|
|
99
|
+
<MyazaButton
|
|
100
|
+
label="Continue"
|
|
101
|
+
loading={flow.confirming}
|
|
102
|
+
disabled={!flow.pin}
|
|
103
|
+
onPress={handleContinue}
|
|
104
|
+
/>
|
|
105
|
+
<SkipForNow requirePin={flow.cfg?.requirePin} onPress={flow.exitForward} />
|
|
106
|
+
</>
|
|
107
|
+
}
|
|
108
|
+
>
|
|
109
|
+
<View>
|
|
110
|
+
{vendorsStubbed ? (
|
|
111
|
+
<AddressMapStub hasPin={Boolean(flow.pin)} onLand={(next) => flow.setPin(next)} defaultCenter={flow.view.center} height={mapHeight} />
|
|
112
|
+
) : (
|
|
113
|
+
<FramedMapPicker
|
|
114
|
+
frameUrl={mapsFrameUrl}
|
|
115
|
+
value={flow.pin}
|
|
116
|
+
onChange={(next) => flow.setPin(next)}
|
|
117
|
+
defaultCenter={flow.view.center}
|
|
118
|
+
defaultZoom={flow.view.zoom}
|
|
119
|
+
height={mapHeight}
|
|
120
|
+
/>
|
|
121
|
+
)}
|
|
122
|
+
{flow.pin ? <LocateChip locating={flow.locating} onPress={() => void flow.locateToPin()} /> : null}
|
|
123
|
+
</View>
|
|
124
|
+
|
|
125
|
+
<View style={{ height: spacing.md }} />
|
|
126
|
+
<PinSummaryRow
|
|
127
|
+
line={address ? displayAddressLine(address) : null}
|
|
128
|
+
labelling={flow.labelling}
|
|
129
|
+
values={detailValuesOf(address)}
|
|
130
|
+
disabled={!flow.pin}
|
|
131
|
+
onPress={() => setSheetOpen(true)}
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
{askLabel && label ? (
|
|
135
|
+
<>
|
|
136
|
+
<View style={{ height: spacing.md }} />
|
|
137
|
+
<LabelDecisionRow label={label} onKeep={flow.keepPickedLabel} onAdopt={flow.adoptPinAddress} />
|
|
138
|
+
</>
|
|
139
|
+
) : null}
|
|
140
|
+
|
|
141
|
+
{flow.pin ? null : (
|
|
142
|
+
<>
|
|
143
|
+
<View style={{ height: spacing.md }} />
|
|
144
|
+
<CurrentLocationRow hint={flow.currentFix?.label ?? null} locating={flow.locating} onPress={() => void flow.applyCurrentFix()} />
|
|
145
|
+
</>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{flow.error ? (
|
|
149
|
+
<MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.sm }}>
|
|
150
|
+
{flow.error}
|
|
151
|
+
</MyazaText>
|
|
152
|
+
) : null}
|
|
153
|
+
{missingNudge && missingRequired.length > 0 ? (
|
|
154
|
+
<MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.sm }}>
|
|
155
|
+
{missingFieldsNudge(missingRequired)}
|
|
156
|
+
</MyazaText>
|
|
157
|
+
) : null}
|
|
158
|
+
</StickyActions>
|
|
159
|
+
|
|
160
|
+
{sheetOpen ? (
|
|
161
|
+
<DetailsSheet
|
|
162
|
+
isBusiness={flow.isBusiness}
|
|
163
|
+
parts={address?.parts ?? null}
|
|
164
|
+
country={flow.country ?? null}
|
|
165
|
+
directionsRequired={flow.cfg?.directions === 'required'}
|
|
166
|
+
addressConfig={flow.cfg}
|
|
167
|
+
values={detailValuesOf(address)}
|
|
168
|
+
disabled={!flow.pin}
|
|
169
|
+
onChange={(patch) => {
|
|
170
|
+
const current = store.getState().address;
|
|
171
|
+
if (current) store.getState().setAddress({ ...current, ...patch });
|
|
172
|
+
}}
|
|
173
|
+
onClose={() => setSheetOpen(false)}
|
|
174
|
+
/>
|
|
175
|
+
) : null}
|
|
176
|
+
</>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius, spacing } from '../../config/theme';
|
|
5
|
+
import { useKyc, useKycStore, useTheme } from '../../components/runtime';
|
|
6
|
+
import { MyazaText } from '../../components/Typography';
|
|
7
|
+
import { MyazaButton } from '../../components/MyazaButton';
|
|
8
|
+
import { displayAddressLine, addressVendorsStubbed } from '../../lib/address-flow';
|
|
9
|
+
import { useAuthedImage } from '../../lib/authed-image';
|
|
10
|
+
import { missingFieldsNudge, missingRequiredAddressFields } from '../../lib/address-field-modes';
|
|
11
|
+
import { useAddressFlow } from './use-address-flow';
|
|
12
|
+
import { AddressSandboxTabs } from './AddressSandboxTabs';
|
|
13
|
+
import { SkipForNow } from './SkipForNow';
|
|
14
|
+
import { ReviewMapPicture } from './ReviewMapPicture';
|
|
15
|
+
import { HERO, SECOND, ReviewEntranceThumb } from './ReviewEntranceThumbs';
|
|
16
|
+
import { ReviewAddressBand } from './ReviewAddressBand';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The commit point, as ONE composed card: a read-only summary map with the
|
|
20
|
+
* entrance photo hanging over its bottom edge like a photo clipped to a
|
|
21
|
+
* document, and the address underneath as the card's own heading. Tapping the
|
|
22
|
+
* map jumps back to the pin step. The presence story lives on the intro
|
|
23
|
+
* screen, not here. Workflow-required details the applicant skipped hold
|
|
24
|
+
* Confirm (the pin step's own gate, repeated here as the backstop). The map
|
|
25
|
+
* surface is ReviewMapPicture, the thumbnails ReviewEntranceThumbs.
|
|
26
|
+
*/
|
|
27
|
+
export function AddressReviewStep(): React.ReactElement {
|
|
28
|
+
const store = useKycStore();
|
|
29
|
+
const { colors } = useTheme();
|
|
30
|
+
const flow = useAddressFlow();
|
|
31
|
+
const previewUri = useKyc((s) => s.addressPhotoPreview);
|
|
32
|
+
const mapsFrameUrl = useKyc((s) => s.serverConfig.mapsFrameUrl ?? null);
|
|
33
|
+
const vendorsStubbed = addressVendorsStubbed({
|
|
34
|
+
environment: useKyc((s) => s.serverConfig.environment ?? null),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// A resumed session can land straight here with a pin that predates the
|
|
38
|
+
// label fields — reverse-geocode it once rather than showing coordinates.
|
|
39
|
+
const { relabelPin } = flow;
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
relabelPin();
|
|
42
|
+
}, [relabelPin]);
|
|
43
|
+
|
|
44
|
+
const address = flow.address;
|
|
45
|
+
const directions = address?.directions.trim() || null;
|
|
46
|
+
|
|
47
|
+
// The framed Street View entrance, drawn through the server: the browser
|
|
48
|
+
// key lives in the framed page, never in this SDK. Fetched with the SDK's
|
|
49
|
+
// bearer by lib/authed-image (an <Image> header is dropped on Android).
|
|
50
|
+
const frame = address?.streetView ?? null;
|
|
51
|
+
const api = useKyc((s) => s.api);
|
|
52
|
+
const frameImage = useAuthedImage(frame ? api.streetViewPreviewSource(frame) : null, 'Street View entrance');
|
|
53
|
+
const frameSource = frameImage.uri ? { uri: frameImage.uri } : null;
|
|
54
|
+
// The map as a picture, through the same bearer; a refused one (the Static
|
|
55
|
+
// API is a separate console enablement) falls back to the live map.
|
|
56
|
+
const mapImage = useAuthedImage(
|
|
57
|
+
flow.pin && !vendorsStubbed ? api.staticMapSource({ ...flow.pin, zoom: 16 }) : null,
|
|
58
|
+
'Map picture',
|
|
59
|
+
);
|
|
60
|
+
const [pictureFailed, setPictureFailed] = useState(false);
|
|
61
|
+
// An entrance thumbnail whose bytes never arrive must not stay on screen as
|
|
62
|
+
// an empty bordered box hanging over the map (the S24, 2026-09-07); it drops
|
|
63
|
+
// out, and the band's clearance with it.
|
|
64
|
+
const [heroFailed, setHeroFailed] = useState(false);
|
|
65
|
+
const [secondFailed, setSecondFailed] = useState(false);
|
|
66
|
+
const failed = (label: string, error: unknown): void => {
|
|
67
|
+
if (__DEV__) console.warn(`${label} failed to load`, error);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// The photo leads when there is one, exactly as on web; the framed view
|
|
71
|
+
// takes the smaller slot beside it, or the big one when it is all there is.
|
|
72
|
+
const hero = previewUri ? { uri: previewUri } : frameSource;
|
|
73
|
+
const second = previewUri && frameSource ? frameSource : null;
|
|
74
|
+
const heroLabel = previewUri ? 'Entrance photo' : 'Street View entrance';
|
|
75
|
+
const heroShown = hero && !heroFailed ? hero : null;
|
|
76
|
+
const secondShown = second && !secondFailed ? second : null;
|
|
77
|
+
const missingRequired = missingRequiredAddressFields(flow.cfg, address);
|
|
78
|
+
const toPin = (): void => store.getState().goToStep('address-collection');
|
|
79
|
+
const line = address ? displayAddressLine(address) : '';
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<View>
|
|
83
|
+
<View style={{ borderWidth: 1, borderColor: colors.border, borderRadius: radius.md }}>
|
|
84
|
+
{flow.pin ? (
|
|
85
|
+
// ABOVE the band, which paints a solid background: without this the
|
|
86
|
+
// entrance hanging over the map's edge was covered by it.
|
|
87
|
+
<View style={{ zIndex: 2 }}>
|
|
88
|
+
<ReviewMapPicture
|
|
89
|
+
pin={flow.pin}
|
|
90
|
+
vendorsStubbed={vendorsStubbed}
|
|
91
|
+
mapsFrameUrl={mapsFrameUrl}
|
|
92
|
+
staticMap={mapImage.uri ? { uri: mapImage.uri } : null}
|
|
93
|
+
staticMapFailed={mapImage.failed || pictureFailed}
|
|
94
|
+
onEdit={toPin}
|
|
95
|
+
onPictureFailed={() => setPictureFailed(true)}
|
|
96
|
+
/>
|
|
97
|
+
{heroShown ? (
|
|
98
|
+
<ReviewEntranceThumb
|
|
99
|
+
source={heroShown}
|
|
100
|
+
label={heroLabel}
|
|
101
|
+
size={HERO}
|
|
102
|
+
right={16}
|
|
103
|
+
onFailed={(e) => {
|
|
104
|
+
failed(heroLabel, e);
|
|
105
|
+
setHeroFailed(true);
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
) : null}
|
|
109
|
+
{secondShown ? (
|
|
110
|
+
<ReviewEntranceThumb
|
|
111
|
+
source={secondShown}
|
|
112
|
+
label="Street View entrance"
|
|
113
|
+
size={SECOND}
|
|
114
|
+
right={144}
|
|
115
|
+
onFailed={(e) => {
|
|
116
|
+
failed('Street View entrance', e);
|
|
117
|
+
setSecondFailed(true);
|
|
118
|
+
}}
|
|
119
|
+
/>
|
|
120
|
+
) : null}
|
|
121
|
+
</View>
|
|
122
|
+
) : null}
|
|
123
|
+
|
|
124
|
+
<ReviewAddressBand
|
|
125
|
+
isBusiness={flow.isBusiness}
|
|
126
|
+
hasAddress={Boolean(address)}
|
|
127
|
+
line={line}
|
|
128
|
+
labelling={flow.labelling}
|
|
129
|
+
directions={directions}
|
|
130
|
+
clearsHero={Boolean(heroShown)}
|
|
131
|
+
onEdit={toPin}
|
|
132
|
+
/>
|
|
133
|
+
</View>
|
|
134
|
+
|
|
135
|
+
{flow.error ? (
|
|
136
|
+
<MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.sm }}>
|
|
137
|
+
{flow.error}
|
|
138
|
+
</MyazaText>
|
|
139
|
+
) : null}
|
|
140
|
+
{missingRequired.length > 0 ? (
|
|
141
|
+
<MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.sm }}>
|
|
142
|
+
{`${missingFieldsNudge(missingRequired)} `}
|
|
143
|
+
<MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }} onPress={toPin}>
|
|
144
|
+
Edit details
|
|
145
|
+
</MyazaText>
|
|
146
|
+
</MyazaText>
|
|
147
|
+
) : null}
|
|
148
|
+
|
|
149
|
+
<View style={{ height: spacing.lg }} />
|
|
150
|
+
<AddressSandboxTabs />
|
|
151
|
+
<View style={{ height: spacing.md }} />
|
|
152
|
+
<MyazaButton
|
|
153
|
+
label="Confirm address"
|
|
154
|
+
loading={flow.confirming}
|
|
155
|
+
disabled={!flow.pin || missingRequired.length > 0}
|
|
156
|
+
onPress={() => void flow.confirm()}
|
|
157
|
+
/>
|
|
158
|
+
|
|
159
|
+
<SkipForNow requirePin={flow.cfg?.requirePin} onPress={flow.exitForward} />
|
|
160
|
+
</View>
|
|
161
|
+
);
|
|
162
|
+
}
|