@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,179 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Image, PanResponder, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius } from '../config/theme';
|
|
5
|
+
import { useTheme } from './runtime';
|
|
6
|
+
import { MapPinMarker } from './MapPinMarker';
|
|
7
|
+
import { MapAttribution, MapZoomControls } from './MapChrome';
|
|
8
|
+
import {
|
|
9
|
+
MAX_ZOOM,
|
|
10
|
+
MIN_ZOOM,
|
|
11
|
+
panCenter,
|
|
12
|
+
visibleTiles,
|
|
13
|
+
type LatLng,
|
|
14
|
+
} from '../lib/map-tiles';
|
|
15
|
+
|
|
16
|
+
interface MapPinPickerProps {
|
|
17
|
+
/** The confirmed pin, when one exists — the map centres on it. */
|
|
18
|
+
value: LatLng | null;
|
|
19
|
+
/** Fired when the user settles the map (drag end / zoom / recentre). */
|
|
20
|
+
onChange: (pin: LatLng) => void;
|
|
21
|
+
defaultCenter: LatLng;
|
|
22
|
+
defaultZoom: number;
|
|
23
|
+
/** Taller on the pin step, where there has to be room to actually find the
|
|
24
|
+
* building; short on the review step's read-only summary. */
|
|
25
|
+
height?: number;
|
|
26
|
+
/**
|
|
27
|
+
* A read-only summary map: no gestures, no zoom controls, no onChange. The
|
|
28
|
+
* review step draws the pin as a picture of the decision, and a map that can
|
|
29
|
+
* be dragged there invites an edit the step is not offering.
|
|
30
|
+
*/
|
|
31
|
+
interactive?: boolean;
|
|
32
|
+
/** Corner rounding — squared off when the map is clipped by a card. */
|
|
33
|
+
cornerRadius?: number;
|
|
34
|
+
/** Zoom when the map has a pin. The pin step goes closer than the default so
|
|
35
|
+
* "is the pin on YOUR building?" is answerable without pinching first. */
|
|
36
|
+
pinZoom?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const MAP_HEIGHT = 256;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A dependency-free OSM slippy map with a FIXED CENTRE PIN — the user moves
|
|
43
|
+
* the map under the pin (the pattern address pickers use on phones), so the
|
|
44
|
+
* pin is always exactly the centre and there is no marker to fumble. Maths in
|
|
45
|
+
* lib/map-tiles.ts; this is only the gesture shell. Mirrors the web SDK's
|
|
46
|
+
* MapPinPicker: tiles for the committed centre, a live translate during the
|
|
47
|
+
* drag, and a commit (+ onChange) on release.
|
|
48
|
+
*/
|
|
49
|
+
export function MapPinPicker({
|
|
50
|
+
value,
|
|
51
|
+
onChange,
|
|
52
|
+
defaultCenter,
|
|
53
|
+
defaultZoom,
|
|
54
|
+
height = MAP_HEIGHT,
|
|
55
|
+
interactive = true,
|
|
56
|
+
cornerRadius,
|
|
57
|
+
pinZoom = 17,
|
|
58
|
+
}: MapPinPickerProps) {
|
|
59
|
+
const { colors } = useTheme();
|
|
60
|
+
const [size, setSize] = useState<{ w: number; h: number } | null>(null);
|
|
61
|
+
const [center, setCenter] = useState<LatLng>(value ?? defaultCenter);
|
|
62
|
+
const [zoom, setZoom] = useState(value ? pinZoom : defaultZoom);
|
|
63
|
+
// Live drag offset — applied as a transform so panning stays smooth; the
|
|
64
|
+
// centre (and tile set) commits on release.
|
|
65
|
+
const [drag, setDrag] = useState<{ dx: number; dy: number } | null>(null);
|
|
66
|
+
|
|
67
|
+
// The responder callbacks close over state, so they read it via a ref — a
|
|
68
|
+
// PanResponder is created once and would otherwise pan against the centre
|
|
69
|
+
// and zoom from the first render forever.
|
|
70
|
+
const live = useRef({ center, zoom, onChange });
|
|
71
|
+
live.current = { center, zoom, onChange };
|
|
72
|
+
|
|
73
|
+
// An external recentre (a search pick, or Use my location) moves the map AND
|
|
74
|
+
// zooms in: "is the pin on your building?" cannot be answered at country
|
|
75
|
+
// zoom, and asking someone to pinch first is asking them to do our work.
|
|
76
|
+
// SELF-caused moves round-trip through the same prop (drag commits via
|
|
77
|
+
// onChange → store → new `value`), so a genuinely-external move is one that
|
|
78
|
+
// differs from the centre the map is already showing — without that check,
|
|
79
|
+
// an applicant who zoomed out to find their area was yanked back to pin
|
|
80
|
+
// zoom on every drag. Flutter's picker documents the same hazard.
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!value) return;
|
|
83
|
+
const c = live.current.center;
|
|
84
|
+
if (Math.abs(c.lat - value.lat) < 1e-9 && Math.abs(c.lng - value.lng) < 1e-9) return;
|
|
85
|
+
setCenter(value);
|
|
86
|
+
setZoom((z) => Math.max(z, pinZoom));
|
|
87
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
88
|
+
}, [value?.lat, value?.lng]);
|
|
89
|
+
|
|
90
|
+
const pan = useRef(
|
|
91
|
+
PanResponder.create({
|
|
92
|
+
onStartShouldSetPanResponder: () => true,
|
|
93
|
+
onMoveShouldSetPanResponder: (_e, g) => Math.abs(g.dx) + Math.abs(g.dy) > 4,
|
|
94
|
+
onPanResponderMove: (_e, g) => setDrag({ dx: g.dx, dy: g.dy }),
|
|
95
|
+
onPanResponderRelease: (_e, g) => {
|
|
96
|
+
setDrag(null);
|
|
97
|
+
if (g.dx === 0 && g.dy === 0) return;
|
|
98
|
+
const { center: c, zoom: z, onChange: fire } = live.current;
|
|
99
|
+
const next = panCenter(c, z, g.dx, g.dy);
|
|
100
|
+
setCenter(next);
|
|
101
|
+
fire(next);
|
|
102
|
+
},
|
|
103
|
+
onPanResponderTerminate: () => setDrag(null),
|
|
104
|
+
// Android: a parent ScrollView (StickyActions) asks to take a vertical
|
|
105
|
+
// drag over once it passes the scroll slop; refuse, and block the native
|
|
106
|
+
// responder, or the map stops dead a few pixels in while the page moves.
|
|
107
|
+
onPanResponderTerminationRequest: () => false,
|
|
108
|
+
onShouldBlockNativeResponder: () => true,
|
|
109
|
+
}),
|
|
110
|
+
).current;
|
|
111
|
+
|
|
112
|
+
const zoomBy = (delta: number) => {
|
|
113
|
+
const next = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom + delta));
|
|
114
|
+
if (next === zoom) return;
|
|
115
|
+
setZoom(next);
|
|
116
|
+
onChange(center);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const tiles = size ? visibleTiles(center, zoom, size.w, size.h) : [];
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<View
|
|
123
|
+
accessibilityLabel={
|
|
124
|
+
interactive ? 'Map. Drag to position the pin on your address.' : 'Map showing your address'
|
|
125
|
+
}
|
|
126
|
+
onLayout={(e) =>
|
|
127
|
+
setSize({ w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height })
|
|
128
|
+
}
|
|
129
|
+
style={{
|
|
130
|
+
height,
|
|
131
|
+
borderRadius: cornerRadius ?? radius.md,
|
|
132
|
+
borderWidth: cornerRadius === 0 ? 0 : 1,
|
|
133
|
+
borderColor: colors.border,
|
|
134
|
+
backgroundColor: colors.backgroundSecondary,
|
|
135
|
+
overflow: 'hidden',
|
|
136
|
+
}}
|
|
137
|
+
{...(interactive ? pan.panHandlers : {})}
|
|
138
|
+
>
|
|
139
|
+
<View
|
|
140
|
+
style={{
|
|
141
|
+
position: 'absolute',
|
|
142
|
+
top: 0,
|
|
143
|
+
left: 0,
|
|
144
|
+
right: 0,
|
|
145
|
+
bottom: 0,
|
|
146
|
+
transform: drag ? [{ translateX: drag.dx }, { translateY: drag.dy }] : [],
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{tiles.map((tile) => (
|
|
150
|
+
<Image
|
|
151
|
+
key={tile.key}
|
|
152
|
+
source={{ uri: tile.url }}
|
|
153
|
+
style={{ position: 'absolute', left: tile.left, top: tile.top, width: 256, height: 256 }}
|
|
154
|
+
/>
|
|
155
|
+
))}
|
|
156
|
+
</View>
|
|
157
|
+
|
|
158
|
+
{/* The fixed centre pin — its stem tip on the exact centre, lifted
|
|
159
|
+
with the landing dot beneath it while the map pans. */}
|
|
160
|
+
<View
|
|
161
|
+
pointerEvents="none"
|
|
162
|
+
style={{
|
|
163
|
+
position: 'absolute',
|
|
164
|
+
left: 0,
|
|
165
|
+
right: 0,
|
|
166
|
+
top: 0,
|
|
167
|
+
bottom: height / 2,
|
|
168
|
+
alignItems: 'center',
|
|
169
|
+
justifyContent: 'flex-end',
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
<MapPinMarker lifted={drag != null} color={colors.primary} />
|
|
173
|
+
</View>
|
|
174
|
+
|
|
175
|
+
{interactive ? <MapZoomControls onZoom={zoomBy} /> : null}
|
|
176
|
+
<MapAttribution />
|
|
177
|
+
</View>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius, spacing } from '../config/theme';
|
|
5
|
+
import { useTheme } from './runtime';
|
|
6
|
+
import { Icon, type IconName } from './Icon';
|
|
7
|
+
import { MyazaText } from './Typography';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// The presence story's three-node track, shared by the intro gate (the promise)
|
|
11
|
+
// and the success card (the same promise, now running) so the two read as two
|
|
12
|
+
// frames of ONE story rather than two unrelated notices.
|
|
13
|
+
//
|
|
14
|
+
// Nodes ahead of the active one are TINTED, not greyed out: they are the plan,
|
|
15
|
+
// not disabled controls. Flat fills only.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export interface Milestone {
|
|
19
|
+
icon: IconName;
|
|
20
|
+
/** The when: "Your part", "Today", "Then". */
|
|
21
|
+
stage: string;
|
|
22
|
+
title: string;
|
|
23
|
+
caption: string;
|
|
24
|
+
state: 'active' | 'ahead';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function MilestoneTrack({
|
|
28
|
+
milestones,
|
|
29
|
+
numbered = false,
|
|
30
|
+
nodeSize = 40,
|
|
31
|
+
}: {
|
|
32
|
+
milestones: readonly Milestone[];
|
|
33
|
+
/** Wear the step number on each node — the sequence is the point on the
|
|
34
|
+
* intro screen, where nothing has happened yet. */
|
|
35
|
+
numbered?: boolean;
|
|
36
|
+
/** Node diameter, and therefore where the connecting rail has to sit: 40 on
|
|
37
|
+
* the primer (web h-10), 32 on the success card (web h-8). */
|
|
38
|
+
nodeSize?: number;
|
|
39
|
+
}): React.ReactElement {
|
|
40
|
+
const { colors } = useTheme();
|
|
41
|
+
const NODE = nodeSize;
|
|
42
|
+
// The active node's halo: web's `shadow-[0_0_0_5px] shadow-primary/15`
|
|
43
|
+
// (4px on the smaller success-card node), drawn as a ring behind it.
|
|
44
|
+
const halo = NODE >= 40 ? 5 : 4;
|
|
45
|
+
return (
|
|
46
|
+
<View style={{ paddingHorizontal: spacing.md, paddingVertical: spacing.md }}>
|
|
47
|
+
{milestones.map((m, i) => {
|
|
48
|
+
const active = m.state === 'active';
|
|
49
|
+
const last = i === milestones.length - 1;
|
|
50
|
+
return (
|
|
51
|
+
<View
|
|
52
|
+
key={m.title}
|
|
53
|
+
style={{ flexDirection: 'row', gap: spacing.md, paddingBottom: last ? 0 : spacing.md }}
|
|
54
|
+
>
|
|
55
|
+
{!last ? (
|
|
56
|
+
<View
|
|
57
|
+
pointerEvents="none"
|
|
58
|
+
style={{
|
|
59
|
+
position: 'absolute',
|
|
60
|
+
left: NODE / 2 - 0.5,
|
|
61
|
+
top: NODE + 4,
|
|
62
|
+
bottom: 0,
|
|
63
|
+
width: 1,
|
|
64
|
+
backgroundColor: `${colors.primary}33`,
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
) : null}
|
|
68
|
+
|
|
69
|
+
<View style={{ width: NODE }}>
|
|
70
|
+
{active ? (
|
|
71
|
+
<View
|
|
72
|
+
pointerEvents="none"
|
|
73
|
+
style={{
|
|
74
|
+
position: 'absolute',
|
|
75
|
+
left: -halo,
|
|
76
|
+
top: -halo,
|
|
77
|
+
width: NODE + halo * 2,
|
|
78
|
+
height: NODE + halo * 2,
|
|
79
|
+
borderRadius: radius.full,
|
|
80
|
+
backgroundColor: `${colors.primary}26`,
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
) : null}
|
|
84
|
+
<View
|
|
85
|
+
style={{
|
|
86
|
+
width: NODE,
|
|
87
|
+
height: NODE,
|
|
88
|
+
borderRadius: radius.full,
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
backgroundColor: active ? colors.primary : `${colors.primary}1A`,
|
|
92
|
+
...(active ? {} : { borderWidth: 1, borderColor: `${colors.primary}33` }),
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<Icon name={m.icon} size={NODE * 0.45} color={active ? colors.onPrimary : colors.primary} />
|
|
96
|
+
</View>
|
|
97
|
+
{numbered ? (
|
|
98
|
+
<View
|
|
99
|
+
style={{
|
|
100
|
+
position: 'absolute',
|
|
101
|
+
right: -4,
|
|
102
|
+
top: -4,
|
|
103
|
+
width: 18,
|
|
104
|
+
height: 18,
|
|
105
|
+
borderRadius: radius.full,
|
|
106
|
+
borderWidth: 1,
|
|
107
|
+
borderColor: active ? colors.primary : colors.border,
|
|
108
|
+
backgroundColor: colors.background,
|
|
109
|
+
alignItems: 'center',
|
|
110
|
+
justifyContent: 'center',
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<MyazaText
|
|
114
|
+
variant="bodySmall"
|
|
115
|
+
color={active ? colors.primary : colors.textSecondary}
|
|
116
|
+
style={{ fontSize: 9, fontWeight: '700', lineHeight: 11 }}
|
|
117
|
+
>
|
|
118
|
+
{i + 1}
|
|
119
|
+
</MyazaText>
|
|
120
|
+
</View>
|
|
121
|
+
) : null}
|
|
122
|
+
</View>
|
|
123
|
+
|
|
124
|
+
<View style={{ flex: 1, paddingTop: 2 }}>
|
|
125
|
+
{/* Uppercased by STYLE, not by the string: a screen reader
|
|
126
|
+
should hear the words, not shouted initials. */}
|
|
127
|
+
<MyazaText
|
|
128
|
+
variant="bodySmall"
|
|
129
|
+
color={active ? colors.primary : colors.textSecondary}
|
|
130
|
+
style={{
|
|
131
|
+
fontSize: 10,
|
|
132
|
+
fontWeight: '600',
|
|
133
|
+
letterSpacing: 0.8,
|
|
134
|
+
textTransform: 'uppercase',
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
{m.stage}
|
|
138
|
+
</MyazaText>
|
|
139
|
+
<MyazaText variant="bodyMedium" style={{ fontWeight: '600', marginTop: 1 }}>
|
|
140
|
+
{m.title}
|
|
141
|
+
</MyazaText>
|
|
142
|
+
<MyazaText
|
|
143
|
+
variant="bodySmall"
|
|
144
|
+
color={colors.textSecondary}
|
|
145
|
+
style={{ marginTop: 2 }}
|
|
146
|
+
>
|
|
147
|
+
{m.caption}
|
|
148
|
+
</MyazaText>
|
|
149
|
+
</View>
|
|
150
|
+
</View>
|
|
151
|
+
);
|
|
152
|
+
})}
|
|
153
|
+
</View>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -21,6 +21,9 @@ export interface MyazaInputProps {
|
|
|
21
21
|
value: string;
|
|
22
22
|
onChangeText: (text: string) => void;
|
|
23
23
|
label?: string;
|
|
24
|
+
/** Marks the label with an asterisk in the error colour: a field the flow
|
|
25
|
+
* will not continue without. */
|
|
26
|
+
required?: boolean;
|
|
24
27
|
placeholder?: string;
|
|
25
28
|
error?: string | null;
|
|
26
29
|
helper?: string;
|
|
@@ -51,6 +54,9 @@ export interface MyazaInputProps {
|
|
|
51
54
|
* keyboard preferred. Defaults on, like the platform.
|
|
52
55
|
*/
|
|
53
56
|
autoCorrect?: boolean;
|
|
57
|
+
/** Multi-line entry (directions, notes): three visible lines, text anchored
|
|
58
|
+
* to the top on Android (which centres multiline text by default). */
|
|
59
|
+
multiline?: boolean;
|
|
54
60
|
onFocus?: FocusHandler;
|
|
55
61
|
onBlur?: BlurHandler;
|
|
56
62
|
}
|
|
@@ -59,6 +65,7 @@ export function MyazaInput({
|
|
|
59
65
|
value,
|
|
60
66
|
onChangeText,
|
|
61
67
|
label,
|
|
68
|
+
required = false,
|
|
62
69
|
placeholder,
|
|
63
70
|
error,
|
|
64
71
|
helper,
|
|
@@ -66,6 +73,7 @@ export function MyazaInput({
|
|
|
66
73
|
autoCapitalize = 'none',
|
|
67
74
|
maxLength,
|
|
68
75
|
editable = true,
|
|
76
|
+
multiline = false,
|
|
69
77
|
autoFocus = false,
|
|
70
78
|
prefix,
|
|
71
79
|
suffix,
|
|
@@ -101,6 +109,11 @@ export function MyazaInput({
|
|
|
101
109
|
{label ? (
|
|
102
110
|
<MyazaText variant="label" style={{ marginBottom: spacing.sm }}>
|
|
103
111
|
{label}
|
|
112
|
+
{required ? (
|
|
113
|
+
<MyazaText variant="label" color={colors.error}>
|
|
114
|
+
{' *'}
|
|
115
|
+
</MyazaText>
|
|
116
|
+
) : null}
|
|
104
117
|
</MyazaText>
|
|
105
118
|
) : null}
|
|
106
119
|
{prefix || suffix ? (
|
|
@@ -152,10 +165,13 @@ export function MyazaInput({
|
|
|
152
165
|
maxLength={maxLength}
|
|
153
166
|
editable={editable}
|
|
154
167
|
autoFocus={autoFocus}
|
|
168
|
+
multiline={multiline}
|
|
169
|
+
numberOfLines={multiline ? 3 : 1}
|
|
170
|
+
textAlignVertical={multiline ? 'top' : 'center'}
|
|
155
171
|
onFocus={handleFocus}
|
|
156
172
|
onBlur={handleBlur}
|
|
157
173
|
style={{
|
|
158
|
-
height: height ?? sizing.inputHeight,
|
|
174
|
+
height: height ?? (multiline ? sizing.inputHeight * 2 : sizing.inputHeight),
|
|
159
175
|
borderWidth,
|
|
160
176
|
borderColor,
|
|
161
177
|
borderRadius: radius.sm,
|
|
@@ -22,27 +22,40 @@ export function OptionRow({
|
|
|
22
22
|
label,
|
|
23
23
|
selected,
|
|
24
24
|
multi,
|
|
25
|
+
role,
|
|
26
|
+
icon,
|
|
25
27
|
leading,
|
|
26
28
|
trailing,
|
|
29
|
+
disabled,
|
|
27
30
|
onPress,
|
|
28
31
|
}: {
|
|
29
32
|
label: string;
|
|
30
33
|
selected: boolean;
|
|
31
34
|
/** Renders a square check instead of a radio dot. */
|
|
32
35
|
multi?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* What the row IS to a screen reader. Defaults from `multi`; a single-pick
|
|
38
|
+
* list wearing the square check (the PoA document kind) passes 'radio'.
|
|
39
|
+
*/
|
|
40
|
+
role?: 'checkbox' | 'radio';
|
|
41
|
+
/** A glyph between the mark and the label — the mark stays. */
|
|
42
|
+
icon?: React.ReactNode;
|
|
33
43
|
/** Something before the label — a flag, an icon. Replaces the radio mark. */
|
|
34
44
|
leading?: React.ReactNode;
|
|
35
45
|
trailing?: React.ReactNode;
|
|
46
|
+
/** Greyed and inert — a choice that is locked, not one that is gone. */
|
|
47
|
+
disabled?: boolean;
|
|
36
48
|
onPress: () => void;
|
|
37
49
|
}): React.ReactElement {
|
|
38
50
|
const { colors } = useTheme();
|
|
39
51
|
return (
|
|
40
52
|
<Pressable
|
|
41
|
-
onPress={onPress}
|
|
42
|
-
accessibilityRole={multi ? 'checkbox' : 'radio'}
|
|
43
|
-
accessibilityState={{ checked: selected }}
|
|
53
|
+
onPress={disabled ? undefined : onPress}
|
|
54
|
+
accessibilityRole={role ?? (multi ? 'checkbox' : 'radio')}
|
|
55
|
+
accessibilityState={{ checked: selected, disabled: disabled === true }}
|
|
44
56
|
accessibilityLabel={label}
|
|
45
57
|
style={{
|
|
58
|
+
opacity: disabled ? 0.5 : 1,
|
|
46
59
|
flexDirection: 'row',
|
|
47
60
|
alignItems: 'center',
|
|
48
61
|
// The checkbox variant matches the web SDK's multi-select card exactly
|
|
@@ -59,6 +72,12 @@ export function OptionRow({
|
|
|
59
72
|
>
|
|
60
73
|
{leading ?? <SelectionMark selected={selected} multi={multi} />}
|
|
61
74
|
<View style={{ width: multi ? 10 : spacing.sm }} />
|
|
75
|
+
{icon ? (
|
|
76
|
+
<>
|
|
77
|
+
{icon}
|
|
78
|
+
<View style={{ width: 10 }} />
|
|
79
|
+
</>
|
|
80
|
+
) : null}
|
|
62
81
|
<MyazaText variant="body" style={{ flex: 1 }}>
|
|
63
82
|
{label}
|
|
64
83
|
</MyazaText>
|
|
@@ -36,6 +36,7 @@ import { COUNTRY_NAMES } from '../config/countryNames.g';
|
|
|
36
36
|
export function PhoneNumberInput({
|
|
37
37
|
value,
|
|
38
38
|
defaultCountry,
|
|
39
|
+
geoCountry,
|
|
39
40
|
disabled,
|
|
40
41
|
autoFocus = false,
|
|
41
42
|
onChange,
|
|
@@ -49,6 +50,12 @@ export function PhoneNumberInput({
|
|
|
49
50
|
value?: string;
|
|
50
51
|
/** Seeds the picker (ISO-2). Falls back to NG. */
|
|
51
52
|
defaultCountry?: string;
|
|
53
|
+
/**
|
|
54
|
+
* The visitor's IP country. Pinned to the top of the picker and tagged, so
|
|
55
|
+
* a guess we made on their behalf is visible AS a guess and one tap away
|
|
56
|
+
* rather than buried at its alphabetical position. Mirrors the web SDK.
|
|
57
|
+
*/
|
|
58
|
+
geoCountry?: string | null;
|
|
52
59
|
disabled?: boolean;
|
|
53
60
|
/** Focus + keyboard on mount. Opt-in: right for a single-field OTP screen,
|
|
54
61
|
* wrong for a row in a company-profile form. */
|
|
@@ -174,6 +181,7 @@ export function PhoneNumberInput({
|
|
|
174
181
|
visible={open}
|
|
175
182
|
options={options}
|
|
176
183
|
selected={country}
|
|
184
|
+
pinned={geoCountry}
|
|
177
185
|
onPick={pickCountry}
|
|
178
186
|
onClose={() => setOpen(false)}
|
|
179
187
|
/>
|
|
@@ -36,12 +36,12 @@ export function PoweredBy({ bottomInset = 0 }: { bottomInset?: number }): React.
|
|
|
36
36
|
return (
|
|
37
37
|
<View
|
|
38
38
|
style={{
|
|
39
|
-
paddingTop:
|
|
39
|
+
paddingTop: 10,
|
|
40
40
|
// Symmetric with the top. It was `lg`, which read as a heavy band on the
|
|
41
41
|
// one screen where vertical space is contested — the searchable country
|
|
42
42
|
// list, where the footer costs a visible row. The home-indicator inset
|
|
43
43
|
// rides on top, so notched devices still clear it comfortably.
|
|
44
|
-
paddingBottom:
|
|
44
|
+
paddingBottom: 12 + bottomInset,
|
|
45
45
|
alignItems: 'center',
|
|
46
46
|
}}
|
|
47
47
|
>
|
|
@@ -70,9 +70,9 @@ export function PoweredBy({ bottomInset = 0 }: { bottomInset?: number }): React.
|
|
|
70
70
|
hitSlop={8}
|
|
71
71
|
style={{ flexDirection: 'row', alignItems: 'center', gap: spacing.sm }}
|
|
72
72
|
>
|
|
73
|
-
<MyazaWordmark height={
|
|
73
|
+
<MyazaWordmark height={24} wordmark={markColor} />
|
|
74
74
|
|
|
75
|
-
<View style={{ width: 1, height:
|
|
75
|
+
<View style={{ width: 1, height: 20, backgroundColor: markColor, opacity: 0.35 }} />
|
|
76
76
|
|
|
77
77
|
{/* ~half the wordmark's height — the ratio the dashboard lockup uses. */}
|
|
78
78
|
{/* Pinned to the brand face rather than inheriting: this word is part
|
|
@@ -83,7 +83,7 @@ export function PoweredBy({ bottomInset = 0 }: { bottomInset?: number }): React.
|
|
|
83
83
|
brandMark
|
|
84
84
|
variant="body"
|
|
85
85
|
color={markColor}
|
|
86
|
-
style={{ flexShrink: 1, fontSize:
|
|
86
|
+
style={{ flexShrink: 1, fontSize: 12, fontWeight: '600', letterSpacing: 1.68 }}
|
|
87
87
|
>
|
|
88
88
|
TRUST
|
|
89
89
|
</MyazaText>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { MyazaText } from './Typography';
|
|
5
|
+
import { MilestoneTrack, type Milestone } from './MilestoneTrack';
|
|
6
|
+
import { useReduceMotion } from './StaggerIn';
|
|
7
|
+
import { radius, spacing } from '../config/theme';
|
|
8
|
+
import { useTheme } from './theme-provider';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// The success screen's presence card, drawn as a LIVE PROCESS rather than a
|
|
12
|
+
// notice: the check began the moment they submitted, so the card should feel
|
|
13
|
+
// like something is already quietly working, because it is.
|
|
14
|
+
//
|
|
15
|
+
// Its milestones are the same three the intro gate promises, one frame later —
|
|
16
|
+
// keep the two, and the web and Flutter SDKs, in lockstep.
|
|
17
|
+
//
|
|
18
|
+
// The standalone "presence consent notice" that used to live here is gone: the
|
|
19
|
+
// disclosures belong on the intro gate, and a second copy on the address step
|
|
20
|
+
// was one more place for the consent wording to drift.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const MILESTONES: readonly Milestone[] = [
|
|
24
|
+
{
|
|
25
|
+
icon: 'map-pin-check',
|
|
26
|
+
stage: 'Today',
|
|
27
|
+
title: 'Check started',
|
|
28
|
+
caption: 'Your pin is saved. Keep location on.',
|
|
29
|
+
state: 'active',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
icon: 'radar',
|
|
33
|
+
stage: 'Next few days',
|
|
34
|
+
title: 'Quiet check-ins',
|
|
35
|
+
caption: 'Your phone confirms it is at your address now and then.',
|
|
36
|
+
state: 'ahead',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
icon: 'bell-ring',
|
|
40
|
+
stage: 'Then',
|
|
41
|
+
title: 'Confirmed',
|
|
42
|
+
caption: 'You get a notification. That is it.',
|
|
43
|
+
state: 'ahead',
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export function PresenceExpectations(): React.ReactElement {
|
|
48
|
+
const { colors } = useTheme();
|
|
49
|
+
return (
|
|
50
|
+
<View
|
|
51
|
+
style={{
|
|
52
|
+
alignSelf: 'stretch',
|
|
53
|
+
marginTop: spacing.lg,
|
|
54
|
+
borderWidth: 1,
|
|
55
|
+
borderColor: `${colors.primary}26`,
|
|
56
|
+
borderRadius: radius.md,
|
|
57
|
+
overflow: 'hidden',
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<View
|
|
61
|
+
style={{
|
|
62
|
+
backgroundColor: `${colors.primary}0F`,
|
|
63
|
+
paddingHorizontal: spacing.md,
|
|
64
|
+
paddingTop: spacing.md,
|
|
65
|
+
paddingBottom: spacing.sm + 4,
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<View
|
|
69
|
+
style={{
|
|
70
|
+
flexDirection: 'row',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
alignSelf: 'flex-start',
|
|
73
|
+
gap: spacing.sm,
|
|
74
|
+
paddingHorizontal: spacing.sm + 2,
|
|
75
|
+
paddingVertical: 4,
|
|
76
|
+
borderRadius: radius.full,
|
|
77
|
+
backgroundColor: `${colors.primary}1A`,
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<LiveDot />
|
|
81
|
+
<MyazaText
|
|
82
|
+
variant="bodySmall"
|
|
83
|
+
color={colors.primary}
|
|
84
|
+
style={{
|
|
85
|
+
fontSize: 11,
|
|
86
|
+
fontWeight: '600',
|
|
87
|
+
letterSpacing: 0.6,
|
|
88
|
+
textTransform: 'uppercase',
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
Address check active
|
|
92
|
+
</MyazaText>
|
|
93
|
+
</View>
|
|
94
|
+
<MyazaText variant="bodyMedium" style={{ fontWeight: '600', marginTop: spacing.sm }}>
|
|
95
|
+
Your address confirms itself from here
|
|
96
|
+
</MyazaText>
|
|
97
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: 2 }}>
|
|
98
|
+
Nothing else for you to do. Carry on as normal.
|
|
99
|
+
</MyazaText>
|
|
100
|
+
</View>
|
|
101
|
+
|
|
102
|
+
{/* Web's h-8 nodes here, against the primer's h-10. */}
|
|
103
|
+
<MilestoneTrack milestones={MILESTONES} nodeSize={32} />
|
|
104
|
+
</View>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The badge's pulse — the one thing on the card that says "running now". */
|
|
109
|
+
function LiveDot(): React.ReactElement {
|
|
110
|
+
const { colors } = useTheme();
|
|
111
|
+
const reduceMotion = useReduceMotion();
|
|
112
|
+
const pulse = useRef(new Animated.Value(0)).current;
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (reduceMotion) return undefined;
|
|
116
|
+
const loop = Animated.loop(
|
|
117
|
+
Animated.sequence([
|
|
118
|
+
Animated.timing(pulse, { toValue: 1, duration: 900, useNativeDriver: true }),
|
|
119
|
+
Animated.timing(pulse, { toValue: 0, duration: 0, useNativeDriver: true }),
|
|
120
|
+
]),
|
|
121
|
+
);
|
|
122
|
+
loop.start();
|
|
123
|
+
return () => loop.stop();
|
|
124
|
+
}, [pulse, reduceMotion]);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<View style={{ width: 8, height: 8, alignItems: 'center', justifyContent: 'center' }}>
|
|
128
|
+
{reduceMotion ? null : (
|
|
129
|
+
<Animated.View
|
|
130
|
+
style={{
|
|
131
|
+
position: 'absolute',
|
|
132
|
+
width: 8,
|
|
133
|
+
height: 8,
|
|
134
|
+
borderRadius: radius.full,
|
|
135
|
+
backgroundColor: colors.primary,
|
|
136
|
+
opacity: pulse.interpolate({ inputRange: [0, 1], outputRange: [0.6, 0] }),
|
|
137
|
+
transform: [
|
|
138
|
+
{ scale: pulse.interpolate({ inputRange: [0, 1], outputRange: [1, 2.2] }) },
|
|
139
|
+
],
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
143
|
+
<View
|
|
144
|
+
style={{
|
|
145
|
+
width: 8,
|
|
146
|
+
height: 8,
|
|
147
|
+
borderRadius: radius.full,
|
|
148
|
+
backgroundColor: colors.primary,
|
|
149
|
+
}}
|
|
150
|
+
/>
|
|
151
|
+
</View>
|
|
152
|
+
);
|
|
153
|
+
}
|