@myazahq/kyc-sdk-react-native 2.5.0 → 3.0.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 +277 -7
- package/android/build.gradle +30 -16
- package/android/consumer-rules.pro +22 -0
- package/android/src/main/AndroidManifest.xml +17 -6
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +27 -70
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +14 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/MlKitModelReadiness.kt +154 -0
- package/app.plugin.js +146 -10
- package/ios/HybridMyazaFaceDetector.swift +8 -3
- package/ios/HybridMyazaTextRecognizer.swift +8 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +21 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +8 -0
- package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +16 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +31 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +2 -0
- package/package.json +30 -9
- package/src/MyazaKYC.tsx +4 -11
- package/src/assets/fonts/Karla_400Regular.ttf +0 -0
- package/src/assets/fonts/Karla_500Medium.ttf +0 -0
- package/src/assets/fonts/Karla_600SemiBold.ttf +0 -0
- package/src/assets/fonts/Karla_700Bold.ttf +0 -0
- package/src/assets/fonts/OFL-Karla.txt +93 -0
- package/src/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
- package/src/assets/fonts/README.md +12 -0
- package/src/assets/fonts/SpaceGrotesk_500Medium.ttf +0 -0
- package/src/assets/fonts/SpaceGrotesk_600SemiBold.ttf +0 -0
- package/src/assets/fonts/SpaceGrotesk_700Bold.ttf +0 -0
- package/src/capture/useAutoCapture.ts +8 -1
- 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/DocumentCropper.tsx +30 -24
- package/src/components/DocumentReview.tsx +11 -1
- package/src/components/DocumentReviewSide.tsx +6 -2
- package/src/components/DocumentReviewZoom.tsx +5 -1
- package/src/components/FramedMapPicker.tsx +122 -0
- package/src/components/GeoBadge.tsx +34 -0
- package/src/components/Icon.tsx +10 -192
- package/src/components/KycFlow.tsx +69 -34
- 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/MyazaButton.tsx +9 -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/RequiredDocumentPill.tsx +114 -0
- package/src/components/StepHeader.tsx +23 -3
- package/src/components/StepView.tsx +17 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/documentReviewCopy.ts +39 -0
- package/src/components/fonts.ts +14 -17
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +48 -5
- 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/documentCaptureMethods.ts +31 -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 +13 -5
- package/src/index.ts +49 -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-copy.ts +51 -0
- package/src/lib/captureRing.ts +83 -0
- package/src/lib/country-adoption.ts +89 -0
- package/src/lib/documentCaptureCheck.ts +136 -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/model-ready.ts +82 -0
- package/src/lib/poa-country-gate.ts +27 -0
- package/src/lib/prime-models.ts +50 -0
- package/src/lib/resubmit.ts +36 -7
- 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-sharpness.ts +211 -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/avatarSource.ts +59 -0
- package/src/liveness/useLiveness.ts +6 -1
- package/src/liveness/useModelReady.ts +9 -55
- package/src/mrz/extract.ts +71 -4
- package/src/mrz/textRecognizer.ts +36 -0
- package/src/mrz/useTextModelReady.ts +21 -0
- package/src/presence/background-math.ts +100 -0
- package/src/presence/background-store.ts +82 -0
- package/src/presence/background.ts +165 -0
- package/src/presence/foreground-service.ts +193 -0
- package/src/presence/fs.ts +51 -0
- package/src/presence/geofence.ts +41 -0
- package/src/presence/math.ts +44 -0
- package/src/presence/post.ts +40 -0
- package/src/presence/report.ts +87 -0
- package/src/presence/sampler.ts +110 -0
- package/src/presence/status.ts +92 -0
- package/src/presence/store.ts +94 -0
- package/src/presence/tier.ts +36 -0
- package/src/presence/watch-wait.ts +81 -0
- package/src/screens/AddressCountryControl.tsx +115 -0
- package/src/screens/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/DocumentCaptureStep.tsx +177 -93
- package/src/screens/LivenessAvatar.tsx +33 -16
- package/src/screens/LivenessStep.tsx +188 -49
- package/src/screens/MrzScanView.tsx +33 -0
- 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/consent/model.ts +79 -10
- package/src/screens/document/CaptureCheckNotice.tsx +77 -0
- package/src/screens/document/UploadPhase.tsx +177 -0
- package/src/screens/document/useDocumentCamera.ts +109 -0
- 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 +8 -4
- package/src/screens/liveness/SelfiePreview.tsx +31 -7
- 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-types.ts +119 -1
- package/src/services/api-verify-types.ts +37 -0
- package/src/services/api.ts +149 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/location.ts +234 -0
- package/src/services/mediaCompress.ts +23 -5
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +7 -0
- package/src/specs/MyazaTextRecognizer.nitro.ts +29 -0
- package/src/store/address.ts +97 -0
- package/src/store/derive.ts +29 -4
- package/src/store/kycStore.ts +120 -5
- 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 +63 -1
- package/src/types/verification.ts +12 -0
- package/src/types/workflow.ts +97 -1
- package/src/assets/liveness/Blink.gif +0 -0
- package/src/assets/liveness/Nod.gif +0 -0
- package/src/assets/liveness/Smile.gif +0 -0
- package/src/assets/liveness/Turn.gif +0 -0
package/src/components/Icon.tsx
CHANGED
|
@@ -1,192 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import Ionicons from '@expo/vector-icons/Ionicons';
|
|
3
|
-
import {
|
|
4
|
-
Info,
|
|
5
|
-
BadgeCheck,
|
|
6
|
-
BookUser,
|
|
7
|
-
Building2,
|
|
8
|
-
Calendar,
|
|
9
|
-
Camera,
|
|
10
|
-
CameraOff,
|
|
11
|
-
ChevronDown,
|
|
12
|
-
ChevronLeft,
|
|
13
|
-
ChevronRight,
|
|
14
|
-
Car,
|
|
15
|
-
Check,
|
|
16
|
-
CircleAlert,
|
|
17
|
-
Contact,
|
|
18
|
-
CreditCard,
|
|
19
|
-
Fingerprint,
|
|
20
|
-
FileText,
|
|
21
|
-
FlaskConical,
|
|
22
|
-
IdCard,
|
|
23
|
-
Image as ImageIcon,
|
|
24
|
-
Landmark,
|
|
25
|
-
Lightbulb,
|
|
26
|
-
Globe,
|
|
27
|
-
Mail,
|
|
28
|
-
Maximize2,
|
|
29
|
-
Lock,
|
|
30
|
-
MessageCircle,
|
|
31
|
-
MessageSquare,
|
|
32
|
-
Moon,
|
|
33
|
-
MoveLeft,
|
|
34
|
-
Nfc,
|
|
35
|
-
Filter,
|
|
36
|
-
Pencil,
|
|
37
|
-
Smartphone,
|
|
38
|
-
RefreshCw,
|
|
39
|
-
ScanFace,
|
|
40
|
-
ScanLine,
|
|
41
|
-
Search,
|
|
42
|
-
Zap,
|
|
43
|
-
ZapOff,
|
|
44
|
-
ShieldCheck,
|
|
45
|
-
Sun,
|
|
46
|
-
Timer,
|
|
47
|
-
Upload,
|
|
48
|
-
Plus,
|
|
49
|
-
UserRound,
|
|
50
|
-
UserRoundPlus,
|
|
51
|
-
Link2,
|
|
52
|
-
Copy,
|
|
53
|
-
Share2,
|
|
54
|
-
UsersRound,
|
|
55
|
-
VideoOff,
|
|
56
|
-
Vote,
|
|
57
|
-
X,
|
|
58
|
-
type LucideIcon,
|
|
59
|
-
} from 'lucide-react-native';
|
|
60
2
|
|
|
61
|
-
|
|
62
|
-
// (`lucide_icons_flutter`) and the web SDK uses (`lucide-react`). Rendered via
|
|
63
|
-
// lucide-react-native (react-native-svg), so the glyphs are pixel-identical
|
|
64
|
-
// across all three platforms.
|
|
3
|
+
import { ICONS, type IconName } from './icon-map';
|
|
65
4
|
|
|
66
|
-
export type IconName
|
|
67
|
-
// chrome
|
|
68
|
-
| 'close'
|
|
69
|
-
| 'back'
|
|
70
|
-
| 'moon'
|
|
71
|
-
| 'sun'
|
|
72
|
-
| 'check'
|
|
73
|
-
| 'lock'
|
|
74
|
-
| 'alert'
|
|
75
|
-
| 'flask'
|
|
76
|
-
| 'refresh'
|
|
77
|
-
| 'maximize'
|
|
78
|
-
| 'calendar'
|
|
79
|
-
| 'image'
|
|
80
|
-
| 'chevron-left'
|
|
81
|
-
| 'chevron-right'
|
|
82
|
-
| 'chevron-down'
|
|
83
|
-
// consent hero + process steps
|
|
84
|
-
| 'shield'
|
|
85
|
-
| 'badge-check'
|
|
86
|
-
| 'building-2'
|
|
87
|
-
| 'user'
|
|
88
|
-
| 'user-plus'
|
|
89
|
-
| 'plus'
|
|
90
|
-
| 'info'
|
|
91
|
-
| 'users'
|
|
92
|
-
| 'scan-line'
|
|
93
|
-
| 'scan-face'
|
|
94
|
-
| 'nfc'
|
|
95
|
-
| 'filter'
|
|
96
|
-
| 'pencil'
|
|
97
|
-
| 'search'
|
|
98
|
-
| 'zap'
|
|
99
|
-
| 'link'
|
|
100
|
-
| 'copy'
|
|
101
|
-
| 'share'
|
|
102
|
-
| 'zap-off'
|
|
103
|
-
| 'timer'
|
|
104
|
-
// id-type glyphs
|
|
105
|
-
| 'fingerprint'
|
|
106
|
-
| 'credit-card'
|
|
107
|
-
| 'globe'
|
|
108
|
-
| 'car'
|
|
109
|
-
| 'vote'
|
|
110
|
-
| 'file-text'
|
|
111
|
-
| 'landmark'
|
|
112
|
-
| 'id-card'
|
|
113
|
-
| 'contact'
|
|
114
|
-
| 'passport'
|
|
115
|
-
| 'camera'
|
|
116
|
-
| 'camera-off'
|
|
117
|
-
| 'video-off'
|
|
118
|
-
| 'upload'
|
|
119
|
-
| 'x'
|
|
120
|
-
| 'lightbulb'
|
|
121
|
-
// contact verification (OTP channels + step footer)
|
|
122
|
-
| 'mail'
|
|
123
|
-
| 'smartphone'
|
|
124
|
-
| 'message-square'
|
|
125
|
-
| 'message-circle';
|
|
126
|
-
|
|
127
|
-
// Maps each name to its Lucide component. Names mirror the Flutter SDK exactly:
|
|
128
|
-
// shieldCheck · badgeCheck · userRound · scanLine · scanFace · fingerprint ·
|
|
129
|
-
// creditCard · globe · car · vote · fileText (+ chrome: X / ArrowLeft /
|
|
130
|
-
// Moon / Sun / Check / Lock / CircleAlert / RefreshCw / Camera).
|
|
131
|
-
const ICONS: Record<IconName, LucideIcon> = {
|
|
132
|
-
close: X,
|
|
133
|
-
back: MoveLeft, // longer-shaft left arrow, matching Flutter's keyboard_backspace
|
|
134
|
-
moon: Moon,
|
|
135
|
-
sun: Sun,
|
|
136
|
-
check: Check,
|
|
137
|
-
lock: Lock,
|
|
138
|
-
alert: CircleAlert,
|
|
139
|
-
// The environment banner's mark, shared with the web SDK (same lucide glyph)
|
|
140
|
-
// and mirrored by Flutter's Icons.science_outlined.
|
|
141
|
-
flask: FlaskConical,
|
|
142
|
-
refresh: RefreshCw,
|
|
143
|
-
maximize: Maximize2,
|
|
144
|
-
calendar: Calendar,
|
|
145
|
-
image: ImageIcon,
|
|
146
|
-
'chevron-left': ChevronLeft,
|
|
147
|
-
'chevron-right': ChevronRight,
|
|
148
|
-
'chevron-down': ChevronDown,
|
|
149
|
-
shield: ShieldCheck,
|
|
150
|
-
'badge-check': BadgeCheck,
|
|
151
|
-
'building-2': Building2,
|
|
152
|
-
user: UserRound,
|
|
153
|
-
info: Info,
|
|
154
|
-
'user-plus': UserRoundPlus,
|
|
155
|
-
plus: Plus,
|
|
156
|
-
link: Link2,
|
|
157
|
-
copy: Copy,
|
|
158
|
-
share: Share2,
|
|
159
|
-
users: UsersRound,
|
|
160
|
-
'scan-line': ScanLine,
|
|
161
|
-
'scan-face': ScanFace,
|
|
162
|
-
nfc: Nfc,
|
|
163
|
-
filter: Filter,
|
|
164
|
-
pencil: Pencil,
|
|
165
|
-
search: Search,
|
|
166
|
-
zap: Zap,
|
|
167
|
-
'zap-off': ZapOff,
|
|
168
|
-
timer: Timer,
|
|
169
|
-
fingerprint: Fingerprint,
|
|
170
|
-
'credit-card': CreditCard,
|
|
171
|
-
globe: Globe,
|
|
172
|
-
car: Car,
|
|
173
|
-
vote: Vote,
|
|
174
|
-
'file-text': FileText,
|
|
175
|
-
landmark: Landmark,
|
|
176
|
-
'id-card': IdCard,
|
|
177
|
-
contact: Contact,
|
|
178
|
-
passport: BookUser,
|
|
179
|
-
camera: Camera,
|
|
180
|
-
'camera-off': CameraOff,
|
|
181
|
-
'video-off': VideoOff,
|
|
182
|
-
upload: Upload,
|
|
183
|
-
x: X,
|
|
184
|
-
lightbulb: Lightbulb,
|
|
185
|
-
mail: Mail,
|
|
186
|
-
smartphone: Smartphone,
|
|
187
|
-
'message-square': MessageSquare,
|
|
188
|
-
'message-circle': MessageCircle,
|
|
189
|
-
};
|
|
5
|
+
export type { IconName } from './icon-map';
|
|
190
6
|
|
|
191
7
|
export interface IconProps {
|
|
192
8
|
name: IconName;
|
|
@@ -198,11 +14,13 @@ export interface IconProps {
|
|
|
198
14
|
}
|
|
199
15
|
|
|
200
16
|
export function Icon({ name, size = 20, color, strokeWidth = 2, solid = false }: IconProps): React.ReactElement {
|
|
201
|
-
// Solid moon/sun for the theme toggle — Lucide has no filled variants, so use
|
|
202
|
-
// Ionicons' purpose-built solid glyphs (matches Flutter's dark_mode/light_mode).
|
|
203
|
-
if (solid && (name === 'moon' || name === 'sun')) {
|
|
204
|
-
return <Ionicons name={name === 'moon' ? 'moon' : 'sunny'} size={size} color={color} />;
|
|
205
|
-
}
|
|
206
17
|
const Glyph = ICONS[name];
|
|
207
|
-
|
|
18
|
+
// The theme toggle's filled moon/sun (Flutter's dark_mode/light_mode). Lucide
|
|
19
|
+
// ships no filled variants, so the glyph's own body takes the colour: the
|
|
20
|
+
// moon's crescent and the sun's disc fill, the sun's rays stay strokes. This
|
|
21
|
+
// replaced an Ionicons import that was the SDK's ONLY use of
|
|
22
|
+
// @expo/vector-icons, a package whose icon fonts still reached every
|
|
23
|
+
// integrator's app for those two glyphs (1.35 MB on a real release APK).
|
|
24
|
+
const fill = solid && (name === 'moon' || name === 'sun') ? color : 'none';
|
|
25
|
+
return <Glyph size={size} color={color} strokeWidth={strokeWidth} fill={fill} />;
|
|
208
26
|
}
|
|
@@ -3,6 +3,7 @@ import { View } from 'react-native';
|
|
|
3
3
|
|
|
4
4
|
import { radius, spacing } from '../config/theme';
|
|
5
5
|
import { buildStepOrder } from '../config/stepOrder';
|
|
6
|
+
import { isAddressStep } from '../lib/address-flow';
|
|
6
7
|
import { stepOrderOptions } from '../store/kycStore';
|
|
7
8
|
import { KYCError } from '../types/verification';
|
|
8
9
|
import { safeReportError } from '../services/errors';
|
|
@@ -50,6 +51,7 @@ export function KycFlow({
|
|
|
50
51
|
const contactChallenge = useKyc((s) => s.contactChallenge);
|
|
51
52
|
const serverConfig = useKyc((s) => s.serverConfig);
|
|
52
53
|
const immersiveCapture = useKyc((s) => s.immersiveCapture);
|
|
54
|
+
const addressIntroSeen = useKyc((s) => s.addressIntroSeen);
|
|
53
55
|
|
|
54
56
|
const startedRef = useRef(false);
|
|
55
57
|
const reportedRef = useRef(false);
|
|
@@ -76,7 +78,26 @@ export function KycFlow({
|
|
|
76
78
|
}
|
|
77
79
|
}, [isFatal, serverConfig.statusCode, serverConfig.message, config.onError]);
|
|
78
80
|
|
|
79
|
-
// ──
|
|
81
|
+
// ── The flow's ordered steps ──────────────────────────────────────────────
|
|
82
|
+
// ONE list, read by the step indicator AND by the address primer below, so
|
|
83
|
+
// neither can describe a flow the user is not walking. This used to be a
|
|
84
|
+
// hand-written copy of the sequence, which meant a step added to the flow
|
|
85
|
+
// silently didn't count towards progress.
|
|
86
|
+
//
|
|
87
|
+
// The derivation is memoised rather than done inside the selector: zustand
|
|
88
|
+
// compares snapshots by identity, so a selector that mints a fresh object on
|
|
89
|
+
// every call re-renders forever.
|
|
90
|
+
const state = useKyc((s) => s);
|
|
91
|
+
const order = useMemo(() => buildStepOrder(stepOrderOptions(state)), [state]);
|
|
92
|
+
|
|
93
|
+
// The presence primer replaces the FIRST address step's body and carries its
|
|
94
|
+
// own title, so the header blanks while it is up. Matched against the order
|
|
95
|
+
// rather than a second copy of "which step comes first".
|
|
96
|
+
const addressIntroPending =
|
|
97
|
+
config.addressCollection?.presence?.enabled === true &&
|
|
98
|
+
!addressIntroSeen &&
|
|
99
|
+
order.find(isAddressStep) === currentStep;
|
|
100
|
+
|
|
80
101
|
const meta = useMemo(
|
|
81
102
|
() =>
|
|
82
103
|
stepHeaderMeta(currentStep, {
|
|
@@ -85,40 +106,55 @@ export function KycFlow({
|
|
|
85
106
|
selectedIdType,
|
|
86
107
|
documentCapturePhase,
|
|
87
108
|
contactChallenge,
|
|
109
|
+
addressIntroPending,
|
|
110
|
+
addressEntranceFraming: state.addressEntranceFraming,
|
|
111
|
+
poaDocumentType: state.poaDocumentType,
|
|
88
112
|
}),
|
|
89
|
-
[
|
|
113
|
+
[
|
|
114
|
+
currentStep,
|
|
115
|
+
config,
|
|
116
|
+
country,
|
|
117
|
+
selectedIdType,
|
|
118
|
+
documentCapturePhase,
|
|
119
|
+
contactChallenge,
|
|
120
|
+
addressIntroPending,
|
|
121
|
+
state.addressEntranceFraming,
|
|
122
|
+
state.poaDocumentType,
|
|
123
|
+
],
|
|
90
124
|
);
|
|
91
125
|
|
|
92
126
|
// ── Step indicator info ───────────────────────────────────────────────────
|
|
93
|
-
// Read from the SAME ordered list navigation uses, so the dots can never
|
|
94
|
-
// describe a different flow than the one the user is walking. This used to be
|
|
95
|
-
// its own hand-written copy of the sequence, which meant a step added to the
|
|
96
|
-
// flow silently didn't count towards progress.
|
|
97
|
-
//
|
|
98
127
|
// 'submitted' is excluded on purpose — it is the terminal screen, not a step
|
|
99
128
|
// to make progress towards, and the indicator is hidden there.
|
|
100
|
-
//
|
|
101
|
-
// The derivation is memoised rather than done inside the selector: zustand
|
|
102
|
-
// compares snapshots by identity, so a selector that mints a fresh object on
|
|
103
|
-
// every call re-renders forever.
|
|
104
|
-
const state = useKyc((s) => s);
|
|
105
129
|
const stepInfo = useMemo(() => {
|
|
106
|
-
if (
|
|
107
|
-
const steps =
|
|
108
|
-
const idx = steps.indexOf(
|
|
130
|
+
if (currentStep === 'submitted') return null;
|
|
131
|
+
const steps = order.filter((s) => s !== 'submitted');
|
|
132
|
+
const idx = steps.indexOf(currentStep);
|
|
109
133
|
if (idx < 0) return null;
|
|
110
134
|
return { progress: (idx + 1) / steps.length, stepCount: steps.length };
|
|
111
|
-
}, [
|
|
135
|
+
}, [currentStep, order]);
|
|
112
136
|
|
|
113
|
-
// The flag beside the title names the country whose IDs are on screen.
|
|
137
|
+
// The flag beside the title names the country whose IDs are on screen. The
|
|
138
|
+
// document steps ask for a country's document just as directly as the ID
|
|
139
|
+
// ones do, so they carry the same flag rather than a second treatment.
|
|
114
140
|
const headerCountry =
|
|
115
|
-
currentStep === 'id-type' ||
|
|
116
|
-
|
|
141
|
+
currentStep === 'id-type' ||
|
|
142
|
+
currentStep === 'id-input' ||
|
|
143
|
+
currentStep === 'document-capture' ||
|
|
144
|
+
currentStep === 'nfc'
|
|
145
|
+
? country
|
|
146
|
+
: null;
|
|
147
|
+
// Back is hidden on the flow's OPENING step (consent, or the first real step
|
|
148
|
+
// when the workflow switched the consent screen off), not on consent by
|
|
149
|
+
// name. A multi-ID run returns to the ID picker for its next check, where
|
|
150
|
+
// Back means "redo the previous one", so a committed slot keeps it.
|
|
151
|
+
const onOpeningStep = currentStep === order[0] && state.multiIdSlots.length === 0;
|
|
152
|
+
const onBack = onOpeningStep || currentStep === 'submitted' ? null : () => store.getState().previousStep();
|
|
117
153
|
|
|
118
154
|
// Android hardware back arrives via the <Modal>'s onRequestClose. Expose a
|
|
119
155
|
// back-aware handler through `backRef` so that handler navigates a step back
|
|
120
156
|
// when the flow can, instead of dismissing the whole SDK. It only reports
|
|
121
|
-
// 'close' from the
|
|
157
|
+
// 'close' from the opening step with close allowed; with
|
|
122
158
|
// `disableClose` set it reports 'blocked' so back can never force the flow
|
|
123
159
|
// closed. (iOS has no hardware back; its swipe-down keeps the standard
|
|
124
160
|
// dismiss behaviour, handled by the Modal.)
|
|
@@ -152,22 +188,21 @@ export function KycFlow({
|
|
|
152
188
|
country={headerCountry}
|
|
153
189
|
onBack={onBack}
|
|
154
190
|
onClose={onClose}
|
|
155
|
-
// The searchable country picker pins its search box
|
|
156
|
-
//
|
|
157
|
-
//
|
|
191
|
+
// The searchable country picker (>5 countries) pins its search box
|
|
192
|
+
// above a ~240-row list; the address pin and entrance steps fill it
|
|
193
|
+
// too, since a map or panorama owns every touch and their actions
|
|
194
|
+
// ride StickyActions at the bottom of a BOUNDED body.
|
|
158
195
|
fillsViewport={
|
|
159
|
-
currentStep === 'country-select' &&
|
|
160
|
-
|
|
196
|
+
(currentStep === 'country-select' &&
|
|
197
|
+
countrySelectOptions({ config, serverConfig }).length > COUNTRY_SEARCH_THRESHOLD) ||
|
|
198
|
+
currentStep === 'address-collection' ||
|
|
199
|
+
currentStep === 'address-entrance'
|
|
161
200
|
}
|
|
162
|
-
// A live camera asks for the whole screen
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
// shape early-returned a different element tree, which changed the
|
|
168
|
-
// step's parent MID-STEP — React unmounted and remounted it, wiping the
|
|
169
|
-
// acknowledged primer and bouncing straight back to it. Scoped to the
|
|
170
|
-
// step as well as the flag so the next step cannot inherit it.
|
|
201
|
+
// A live camera asks for the whole screen (a camera you have to scroll
|
|
202
|
+
// to is a broken camera). Passed as a PROP rather than rendered as its
|
|
203
|
+
// own tree: an early-returned tree changed the step's parent MID-STEP,
|
|
204
|
+
// so React remounted it and wiped the acknowledged primer. Scoped to
|
|
205
|
+
// the step as well as the flag so the next step cannot inherit it.
|
|
171
206
|
immersive={immersiveCapture && currentStep === 'document-capture'}
|
|
172
207
|
>
|
|
173
208
|
<StepView step={currentStep} onClose={onClose} />
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius } from '../config/theme';
|
|
5
|
+
import { useTheme } from './runtime';
|
|
6
|
+
import { MyazaText } from './Typography';
|
|
7
|
+
import { useReduceMotion } from './StaggerIn';
|
|
8
|
+
|
|
9
|
+
// ─── A text line that is on its way ─────────────────────────────────────────
|
|
10
|
+
//
|
|
11
|
+
// Drawn at the line's own height so the card around it does not move when the
|
|
12
|
+
// words land, and shaped like the answer (a rounded bar about the width of a
|
|
13
|
+
// short address) rather than a spinner: a spinner beside grey text says
|
|
14
|
+
// "busy", a bar where the text goes says "the line is coming" (user decision
|
|
15
|
+
// 2026-09-07). The pin summary and the review card use it while the reverse
|
|
16
|
+
// geocode is out. It pulses the way KeyPeoplePending's ghost roster does, so
|
|
17
|
+
// the SDK has one skeleton language, and holds still under reduced motion.
|
|
18
|
+
// The words still reach assistive tech through the live-region label.
|
|
19
|
+
//
|
|
20
|
+
// Mirrors the web SDK's components/LineSkeleton and Flutter's
|
|
21
|
+
// widgets/line_skeleton.dart.
|
|
22
|
+
|
|
23
|
+
type Variant = React.ComponentProps<typeof MyazaText>['variant'];
|
|
24
|
+
|
|
25
|
+
export function LineSkeleton({
|
|
26
|
+
label,
|
|
27
|
+
variant = 'bodyMedium',
|
|
28
|
+
textStyle,
|
|
29
|
+
barHeight = 10,
|
|
30
|
+
width = '62%',
|
|
31
|
+
style,
|
|
32
|
+
}: {
|
|
33
|
+
/** What a screen reader hears, e.g. "Finding the address…". */
|
|
34
|
+
label: string;
|
|
35
|
+
/** The variant the real line renders with, so the ghost owns its height. */
|
|
36
|
+
variant?: Variant;
|
|
37
|
+
textStyle?: object;
|
|
38
|
+
barHeight?: number;
|
|
39
|
+
/** How much of the line the bar covers; an address, not a paragraph. */
|
|
40
|
+
width?: number | `${number}%`;
|
|
41
|
+
style?: object;
|
|
42
|
+
}): React.ReactElement {
|
|
43
|
+
const { colors } = useTheme();
|
|
44
|
+
const reduceMotion = useReduceMotion();
|
|
45
|
+
const pulse = useRef(new Animated.Value(1)).current;
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (reduceMotion) return undefined;
|
|
49
|
+
const loop = Animated.loop(
|
|
50
|
+
Animated.sequence([
|
|
51
|
+
Animated.timing(pulse, { toValue: 0.45, duration: 600, useNativeDriver: true }),
|
|
52
|
+
Animated.timing(pulse, { toValue: 1, duration: 600, useNativeDriver: true }),
|
|
53
|
+
]),
|
|
54
|
+
);
|
|
55
|
+
loop.start();
|
|
56
|
+
return () => loop.stop();
|
|
57
|
+
}, [pulse, reduceMotion]);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<View
|
|
61
|
+
accessible
|
|
62
|
+
accessibilityLiveRegion="polite"
|
|
63
|
+
accessibilityLabel={label}
|
|
64
|
+
style={[{ justifyContent: 'center' }, style]}
|
|
65
|
+
>
|
|
66
|
+
{/* An invisible line in the real variant owns the height, so the card
|
|
67
|
+
does not move when the words replace the bar. */}
|
|
68
|
+
<MyazaText variant={variant} style={[textStyle, { opacity: 0 }]} numberOfLines={1}>
|
|
69
|
+
{' '}
|
|
70
|
+
</MyazaText>
|
|
71
|
+
<Animated.View
|
|
72
|
+
pointerEvents="none"
|
|
73
|
+
style={{
|
|
74
|
+
position: 'absolute',
|
|
75
|
+
left: 0,
|
|
76
|
+
width,
|
|
77
|
+
height: barHeight,
|
|
78
|
+
borderRadius: radius.full,
|
|
79
|
+
backgroundColor: colors.primary100,
|
|
80
|
+
opacity: reduceMotion ? 0.7 : pulse,
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The resolved line fading in where the skeleton was, so the swap reads as
|
|
88
|
+
* the answer landing rather than a flicker. Instant under reduced motion. */
|
|
89
|
+
export function LineReveal({ children, style }: { children: React.ReactNode; style?: object }): React.ReactElement {
|
|
90
|
+
const reduceMotion = useReduceMotion();
|
|
91
|
+
const opacity = useRef(new Animated.Value(0)).current;
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (reduceMotion) {
|
|
94
|
+
opacity.setValue(1);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
Animated.timing(opacity, { toValue: 1, duration: 200, useNativeDriver: true }).start();
|
|
98
|
+
}, [opacity, reduceMotion]);
|
|
99
|
+
return <Animated.View style={[{ opacity }, style]}>{children}</Animated.View>;
|
|
100
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius } from '../config/theme';
|
|
5
|
+
import { useTheme } from './runtime';
|
|
6
|
+
import { Icon } from './Icon';
|
|
7
|
+
import { MyazaText } from './Typography';
|
|
8
|
+
|
|
9
|
+
// The map's furniture, split from MapPinPicker per the 200-line rule.
|
|
10
|
+
|
|
11
|
+
/** Zoom controls, top-right. Absent on a read-only summary map. */
|
|
12
|
+
export function MapZoomControls({ onZoom }: { onZoom: (delta: number) => void }): React.ReactElement {
|
|
13
|
+
const { colors } = useTheme();
|
|
14
|
+
return (
|
|
15
|
+
<View
|
|
16
|
+
style={{
|
|
17
|
+
position: 'absolute',
|
|
18
|
+
right: 8,
|
|
19
|
+
top: 8,
|
|
20
|
+
borderRadius: radius.sm,
|
|
21
|
+
borderWidth: 1,
|
|
22
|
+
borderColor: colors.border,
|
|
23
|
+
backgroundColor: colors.background,
|
|
24
|
+
overflow: 'hidden',
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<Pressable
|
|
28
|
+
accessibilityRole="button"
|
|
29
|
+
accessibilityLabel="Zoom in"
|
|
30
|
+
onPress={() => onZoom(1)}
|
|
31
|
+
style={{ padding: 8 }}
|
|
32
|
+
>
|
|
33
|
+
<Icon name="plus" size={16} color={colors.textDark} />
|
|
34
|
+
</Pressable>
|
|
35
|
+
<Pressable
|
|
36
|
+
accessibilityRole="button"
|
|
37
|
+
accessibilityLabel="Zoom out"
|
|
38
|
+
onPress={() => onZoom(-1)}
|
|
39
|
+
style={{ padding: 8, borderTopWidth: 1, borderTopColor: colors.border }}
|
|
40
|
+
>
|
|
41
|
+
<Icon name="minus" size={16} color={colors.textDark} />
|
|
42
|
+
</Pressable>
|
|
43
|
+
</View>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** OSM tile-usage terms require this to be visible wherever the tiles are. */
|
|
48
|
+
export function MapAttribution(): React.ReactElement {
|
|
49
|
+
const { colors } = useTheme();
|
|
50
|
+
return (
|
|
51
|
+
<View
|
|
52
|
+
pointerEvents="none"
|
|
53
|
+
style={{
|
|
54
|
+
position: 'absolute',
|
|
55
|
+
right: 6,
|
|
56
|
+
bottom: 4,
|
|
57
|
+
paddingHorizontal: 4,
|
|
58
|
+
borderRadius: 3,
|
|
59
|
+
backgroundColor: colors.background + 'B3',
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<MyazaText variant="bodySmall" style={{ fontSize: 10 }} color={colors.textSecondary}>
|
|
63
|
+
© OpenStreetMap contributors
|
|
64
|
+
</MyazaText>
|
|
65
|
+
</View>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import Svg, { Circle, Rect } from 'react-native-svg';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// The Myaza map pin — the same drawing as the web SDK's MapPinMarker and
|
|
7
|
+
// Flutter's map_pin_marker.dart, so the built-in OSM picker wears the pin the
|
|
8
|
+
// framed Google map already does. A 1:1 copy of the Bolt reference: head 46px,
|
|
9
|
+
// eye 0.31, stem 0.098 wide showing 0.41 below the head, no casings, flat body
|
|
10
|
+
// (no shadow on the pin itself).
|
|
11
|
+
//
|
|
12
|
+
// The GROUND DOT is the drag-state shadow: while the map pans the pin lifts
|
|
13
|
+
// 17px and the dot appears at the landing point beneath it; when the pin
|
|
14
|
+
// lands the dot goes, leaving the stem tip resting exactly on the picked
|
|
15
|
+
// coordinate. The parent aligns this widget's BOTTOM edge to the map centre.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export const PIN_LIFT = 17;
|
|
19
|
+
|
|
20
|
+
export function MapPinMarker({
|
|
21
|
+
lifted,
|
|
22
|
+
color,
|
|
23
|
+
}: {
|
|
24
|
+
/** The map is mid-pan: the pin floats and the landing dot shows. */
|
|
25
|
+
lifted: boolean;
|
|
26
|
+
/** The workflow's primary colour (the whole pin wears it). */
|
|
27
|
+
color: string;
|
|
28
|
+
}): React.ReactElement {
|
|
29
|
+
return (
|
|
30
|
+
<View style={{ alignItems: 'center' }} pointerEvents="none">
|
|
31
|
+
<View
|
|
32
|
+
style={{
|
|
33
|
+
position: 'absolute',
|
|
34
|
+
bottom: -2.5,
|
|
35
|
+
width: 11,
|
|
36
|
+
height: 5,
|
|
37
|
+
borderRadius: 3,
|
|
38
|
+
backgroundColor: 'rgba(7, 3, 48, 0.5)',
|
|
39
|
+
opacity: lifted ? 1 : 0,
|
|
40
|
+
}}
|
|
41
|
+
/>
|
|
42
|
+
<Svg
|
|
43
|
+
width={46}
|
|
44
|
+
height={65}
|
|
45
|
+
viewBox="0 0 46 65"
|
|
46
|
+
style={{ transform: [{ translateY: lifted ? -PIN_LIFT : 0 }] }}
|
|
47
|
+
>
|
|
48
|
+
<Rect x={20.75} y={42} width={4.5} height={23} rx={2.25} fill={color} />
|
|
49
|
+
<Circle cx={23} cy={23} r={23} fill={color} />
|
|
50
|
+
<Circle cx={23} cy={23} r={7} fill="#FFFFFF" />
|
|
51
|
+
</Svg>
|
|
52
|
+
</View>
|
|
53
|
+
);
|
|
54
|
+
}
|