@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius, spacing } from '../config/theme';
|
|
5
|
+
import { Icon } from './Icon';
|
|
6
|
+
import { useTheme } from './runtime';
|
|
7
|
+
import { MyazaText } from './Typography';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* "Required: <document>" — which document this step expects, on the screens
|
|
11
|
+
* either side of the camera (the ready primer and the review).
|
|
12
|
+
*
|
|
13
|
+
* The header names the document too, but this is the last thing in view when
|
|
14
|
+
* the applicant is deciding whether they have the right one in hand, and on a
|
|
15
|
+
* two-sided ID it is the only place that says which SIDE is being asked for
|
|
16
|
+
* and how far through they are.
|
|
17
|
+
*
|
|
18
|
+
* Mirrors the web SDK's badge in DocumentCaptureStep and Flutter's
|
|
19
|
+
* `_RequiredPill`: same Lucide glyph, same primary tint, same wording.
|
|
20
|
+
*/
|
|
21
|
+
export function RequiredDocumentPill({
|
|
22
|
+
idTypeLabel,
|
|
23
|
+
sideBadge,
|
|
24
|
+
stepLabel,
|
|
25
|
+
}: {
|
|
26
|
+
idTypeLabel: string;
|
|
27
|
+
/** "Front Side" / "Back Side" — two-sided documents only. */
|
|
28
|
+
sideBadge?: string | null;
|
|
29
|
+
/** "Step 1 of 2" — two-sided documents only. */
|
|
30
|
+
stepLabel?: string | null;
|
|
31
|
+
}): React.ReactElement {
|
|
32
|
+
const { colors } = useTheme();
|
|
33
|
+
const hasMeta = Boolean(sideBadge || stepLabel);
|
|
34
|
+
return (
|
|
35
|
+
<View
|
|
36
|
+
style={{
|
|
37
|
+
flexDirection: 'row',
|
|
38
|
+
alignItems: 'center',
|
|
39
|
+
flexWrap: 'wrap',
|
|
40
|
+
// Column and row gaps are set separately: when this wraps, the two
|
|
41
|
+
// groups need breathing room vertically too, and a single `gap` gives
|
|
42
|
+
// the wrapped row the same spacing as the inline one by accident.
|
|
43
|
+
columnGap: spacing.sm,
|
|
44
|
+
rowGap: spacing.xs,
|
|
45
|
+
paddingHorizontal: spacing.md,
|
|
46
|
+
paddingVertical: spacing.sm,
|
|
47
|
+
borderRadius: radius.md,
|
|
48
|
+
borderWidth: 1,
|
|
49
|
+
borderColor: colors.primary200,
|
|
50
|
+
backgroundColor: colors.primary50,
|
|
51
|
+
}}
|
|
52
|
+
// One statement to a screen reader; the parts read as a sentence
|
|
53
|
+
// rather than three disconnected fragments.
|
|
54
|
+
accessible
|
|
55
|
+
accessibilityLabel={[`Required: ${idTypeLabel}`, sideBadge, stepLabel]
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.join(', ')}
|
|
58
|
+
>
|
|
59
|
+
{/* What the applicant has to be holding. Shrinks and wraps within its
|
|
60
|
+
own box before it pushes anything else off the line, because the
|
|
61
|
+
document name is the one part of this that cannot be abbreviated —
|
|
62
|
+
"Permanent Voter's Card" truncated is a different document. */}
|
|
63
|
+
<View
|
|
64
|
+
style={{
|
|
65
|
+
flexDirection: 'row',
|
|
66
|
+
alignItems: 'center',
|
|
67
|
+
gap: spacing.xs,
|
|
68
|
+
flexShrink: 1,
|
|
69
|
+
minWidth: 0,
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<Icon name="credit-card" size={16} color={colors.primary} />
|
|
73
|
+
<MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>
|
|
74
|
+
Required:
|
|
75
|
+
</MyazaText>
|
|
76
|
+
<MyazaText variant="bodySmall" color={colors.primary} style={{ flexShrink: 1 }}>
|
|
77
|
+
{idTypeLabel}
|
|
78
|
+
</MyazaText>
|
|
79
|
+
</View>
|
|
80
|
+
{/* Side and progress travel TOGETHER. They are one thought — which half
|
|
81
|
+
of a two-sided document this is — and on a narrow screen they drop to
|
|
82
|
+
a second line as a pair.
|
|
83
|
+
The step label used to carry `marginLeft: 'auto'`, which fights
|
|
84
|
+
`flexWrap`: auto-margin assumes spare space on the line, wrapping
|
|
85
|
+
means there is none. So on a long document name it wrapped alone and
|
|
86
|
+
was then flung to the right edge, leaving an orphaned "Step 1 of 2"
|
|
87
|
+
under an empty gap. Seen on a Galaxy S24 with "Permanent Voter's
|
|
88
|
+
Card". Grouping them makes the wrap look deliberate instead. */}
|
|
89
|
+
{hasMeta ? (
|
|
90
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: spacing.xs }}>
|
|
91
|
+
{sideBadge ? (
|
|
92
|
+
<View
|
|
93
|
+
style={{
|
|
94
|
+
paddingHorizontal: spacing.sm,
|
|
95
|
+
paddingVertical: 2,
|
|
96
|
+
borderRadius: radius.full,
|
|
97
|
+
backgroundColor: colors.primary100,
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>
|
|
101
|
+
{sideBadge}
|
|
102
|
+
</MyazaText>
|
|
103
|
+
</View>
|
|
104
|
+
) : null}
|
|
105
|
+
{stepLabel ? (
|
|
106
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary}>
|
|
107
|
+
{stepLabel}
|
|
108
|
+
</MyazaText>
|
|
109
|
+
) : null}
|
|
110
|
+
</View>
|
|
111
|
+
) : null}
|
|
112
|
+
</View>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -17,6 +17,10 @@ import { CountryFlag } from './CountryFlag';
|
|
|
17
17
|
const BACK_SIZE = 40;
|
|
18
18
|
const BACK_FOOTPRINT = BACK_SIZE + spacing.sm;
|
|
19
19
|
|
|
20
|
+
// One line of heading2 (fontSize 20). The flag is centred inside a box of this
|
|
21
|
+
// height so it lands on the first line whether the title wraps or not.
|
|
22
|
+
const FLAG_LINE_HEIGHT = 26;
|
|
23
|
+
|
|
20
24
|
export interface StepHeaderProps {
|
|
21
25
|
title: string;
|
|
22
26
|
description?: string | null;
|
|
@@ -44,13 +48,29 @@ export function StepHeader({ title, description, onBack, country }: StepHeaderPr
|
|
|
44
48
|
/>
|
|
45
49
|
</View>
|
|
46
50
|
) : null}
|
|
47
|
-
|
|
51
|
+
{/* `flex-start`, not `center`. A long document name wraps to two lines
|
|
52
|
+
on a small screen ("Capture Your Permanent Voter's Card" does at
|
|
53
|
+
360dp), and centring the flag against the whole block floated it in
|
|
54
|
+
the gap between the lines, attached to neither. Anchored to the top
|
|
55
|
+
it reads as a marker on the title, wherever the text wraps.
|
|
56
|
+
Seen on a Galaxy S24. */}
|
|
57
|
+
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'flex-start' }}>
|
|
48
58
|
<MyazaText variant="heading2" numberOfLines={2} style={{ flexShrink: 1 }}>
|
|
49
59
|
{title}
|
|
50
60
|
</MyazaText>
|
|
51
61
|
{country ? (
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
// The flag box is the height of one line of heading2, with the flag
|
|
63
|
+
// centred in it — so it sits on the first line's optical centre
|
|
64
|
+
// rather than its top edge, and does not have to be re-nudged if
|
|
65
|
+
// the type scale moves.
|
|
66
|
+
<View
|
|
67
|
+
style={{
|
|
68
|
+
marginLeft: spacing.sm,
|
|
69
|
+
height: FLAG_LINE_HEIGHT,
|
|
70
|
+
justifyContent: 'center',
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<CountryFlag country={country} size={18} />
|
|
54
74
|
</View>
|
|
55
75
|
) : null}
|
|
56
76
|
</View>
|
|
@@ -12,6 +12,12 @@ import { SubmittedStep } from '../screens/SubmittedStep';
|
|
|
12
12
|
import { QuestionnaireStep } from '../screens/QuestionnaireStep';
|
|
13
13
|
import { ContactVerificationStep } from '../screens/ContactVerificationStep';
|
|
14
14
|
import { ProofOfAddressStep } from '../screens/ProofOfAddressStep';
|
|
15
|
+
import {
|
|
16
|
+
AddressEntranceStep,
|
|
17
|
+
AddressPinStep,
|
|
18
|
+
AddressReviewStep,
|
|
19
|
+
AddressSearchStep,
|
|
20
|
+
} from '../screens/address';
|
|
15
21
|
import { CountrySelectStep } from '../screens/CountrySelectStep';
|
|
16
22
|
import { BusinessDetailsStep } from '../screens/BusinessDetailsStep';
|
|
17
23
|
import { BusinessKeyPeopleStep } from '../screens/BusinessKeyPeopleStep';
|
|
@@ -78,6 +84,17 @@ function stepScreen(step: KYCStep, onClose: () => void): React.ReactElement {
|
|
|
78
84
|
return <ContactVerificationStep key="contact-phone" channel="phone" />;
|
|
79
85
|
case 'proof-of-address':
|
|
80
86
|
return <ProofOfAddressStep />;
|
|
87
|
+
// The address flow, in order: find it, confirm it, show it, commit it.
|
|
88
|
+
// 'address-collection' is the PIN step and keeps that wire name so older
|
|
89
|
+
// session progress restores cleanly.
|
|
90
|
+
case 'address-search':
|
|
91
|
+
return <AddressSearchStep />;
|
|
92
|
+
case 'address-collection':
|
|
93
|
+
return <AddressPinStep />;
|
|
94
|
+
case 'address-entrance':
|
|
95
|
+
return <AddressEntranceStep />;
|
|
96
|
+
case 'address-review':
|
|
97
|
+
return <AddressReviewStep />;
|
|
81
98
|
case 'country-select':
|
|
82
99
|
return <CountrySelectStep />;
|
|
83
100
|
case 'business-details':
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ScrollView, StyleSheet, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../config/theme';
|
|
5
|
+
import { useTheme } from './runtime';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A step's primary actions, held at the bottom edge of a viewport-filling
|
|
9
|
+
* body. Mirrors the web SDK's StickyActions.
|
|
10
|
+
*
|
|
11
|
+
* A step that carries a map or a street panorama fills a phone with a surface
|
|
12
|
+
* that OWNS every touch: dragging it moves the map, never the page, so on a
|
|
13
|
+
* small screen the Continue button beneath it could only be reached by finding
|
|
14
|
+
* a strip of margin to scroll on. Holding the actions under a bounded scroll
|
|
15
|
+
* view means the applicant never has to get past the map to move on, and
|
|
16
|
+
* everything else still scrolls under them.
|
|
17
|
+
*
|
|
18
|
+
* Needs a BOUNDED parent: the step must be listed in KycFlow's fillsViewport
|
|
19
|
+
* set, where the sheet hands it a flex:1 box padded `spacing.md`. The footer's
|
|
20
|
+
* negative margins cancel that padding so the bar meets the sheet's edge, and
|
|
21
|
+
* the same padding is restored inside so the buttons keep their gutter.
|
|
22
|
+
*/
|
|
23
|
+
export function StickyActions({
|
|
24
|
+
children,
|
|
25
|
+
actions,
|
|
26
|
+
}: {
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
actions: React.ReactNode;
|
|
29
|
+
}): React.ReactElement {
|
|
30
|
+
const { colors } = useTheme();
|
|
31
|
+
return (
|
|
32
|
+
<View style={{ flex: 1 }}>
|
|
33
|
+
<ScrollView
|
|
34
|
+
style={{ flex: 1 }}
|
|
35
|
+
contentContainerStyle={{ paddingBottom: spacing.sm }}
|
|
36
|
+
keyboardShouldPersistTaps="handled"
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</ScrollView>
|
|
40
|
+
<View
|
|
41
|
+
style={{
|
|
42
|
+
marginHorizontal: -spacing.md,
|
|
43
|
+
marginBottom: -spacing.md,
|
|
44
|
+
paddingHorizontal: spacing.md,
|
|
45
|
+
paddingTop: spacing.sm,
|
|
46
|
+
paddingBottom: spacing.md,
|
|
47
|
+
backgroundColor: colors.background,
|
|
48
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
49
|
+
borderTopColor: colors.border,
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
{actions}
|
|
53
|
+
</View>
|
|
54
|
+
</View>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { groupCountriesByRegion, pinGeoRow } from '../config/regions';
|
|
2
|
+
import type { DialCodeOption } from './DialCodePicker';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// What the country sheet lists, as data. Extracted from DialCodePicker
|
|
6
|
+
// (200-line rule) so the FlatList only renders: the search, the pinned geo
|
|
7
|
+
// row and the optional REGION grouping all decide here, and a unit test reads
|
|
8
|
+
// the result without mounting a sheet. Grouping is what the address-scope
|
|
9
|
+
// country control asks for (user decision 2026-09-06): its sheet reads like
|
|
10
|
+
// the web's region menu and the country-select step — a continent header,
|
|
11
|
+
// then its countries A–Z — rather than one 240-row alphabet. Mirrors
|
|
12
|
+
// Flutter's dial_code_rows.dart.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export type DialCodeItem =
|
|
16
|
+
| { kind: 'row'; option: DialCodeOption }
|
|
17
|
+
| { kind: 'header'; region: string };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Match the name, the ISO code, or the dial code with or without its '+',
|
|
21
|
+
* because people search for "+234", "234" and "Nigeria" in equal measure.
|
|
22
|
+
*/
|
|
23
|
+
export function filterDialCodeOptions(options: DialCodeOption[], query: string): DialCodeOption[] {
|
|
24
|
+
const q = query.trim().toLowerCase();
|
|
25
|
+
if (!q) return options;
|
|
26
|
+
const bare = q.replace(/^\+/, '');
|
|
27
|
+
return options.filter(
|
|
28
|
+
(o) =>
|
|
29
|
+
o.name.toLowerCase().includes(q) ||
|
|
30
|
+
o.code.toLowerCase() === q ||
|
|
31
|
+
(o.dialCode != null && o.dialCode.replace(/^\+/, '').startsWith(bare)),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The sheet's items over the ALREADY-FILTERED options: where they appear to be
|
|
37
|
+
* first (lifted out of the alphabet, still subject to the search), then the
|
|
38
|
+
* rest — flat in the given order, or under region headers when `grouped`.
|
|
39
|
+
*/
|
|
40
|
+
export function buildDialCodeItems(
|
|
41
|
+
visible: DialCodeOption[],
|
|
42
|
+
geoCountry: string | null | undefined,
|
|
43
|
+
grouped: boolean,
|
|
44
|
+
): { geo: DialCodeOption | null; items: DialCodeItem[] } {
|
|
45
|
+
const { pinned: geo, rest } = pinGeoRow(visible, geoCountry, (o) => o.code);
|
|
46
|
+
const items: DialCodeItem[] = geo ? [{ kind: 'row', option: geo }] : [];
|
|
47
|
+
if (!grouped) {
|
|
48
|
+
for (const option of rest) items.push({ kind: 'row', option });
|
|
49
|
+
return { geo, items };
|
|
50
|
+
}
|
|
51
|
+
const byCode = new Map(rest.map((o) => [o.code.toUpperCase(), o]));
|
|
52
|
+
for (const group of groupCountriesByRegion(rest.map((o) => o.code))) {
|
|
53
|
+
items.push({ kind: 'header', region: group.region });
|
|
54
|
+
for (const entry of group.countries) {
|
|
55
|
+
const option = byCode.get(entry.code);
|
|
56
|
+
if (option) items.push({ kind: 'row', option });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { geo, items };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function dialCodeItemKey(item: DialCodeItem): string {
|
|
63
|
+
return item.kind === 'header' ? `region:${item.region}` : item.option.code;
|
|
64
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// ─── The review screen's words, per capture mode ──────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// The document review is shared by both ways a document gets in. "Retake" and
|
|
4
|
+
// "captured" describe a camera, which an upload-only workflow
|
|
5
|
+
// (`allowDocumentScan: false`) never shows the applicant, so that mode says
|
|
6
|
+
// "Replace" and "added" instead, matching the web SDK. Scan mode (scan-only and
|
|
7
|
+
// both-on) keeps its words exactly.
|
|
8
|
+
//
|
|
9
|
+
// Pure, so the wording is pinned by a test without mounting React Native.
|
|
10
|
+
|
|
11
|
+
export type DocumentCaptureMode = 'scan' | 'upload';
|
|
12
|
+
|
|
13
|
+
export interface DocumentReviewCopy {
|
|
14
|
+
/** The line above the thumbnails saying the capture is complete. */
|
|
15
|
+
status: string;
|
|
16
|
+
/** The labelled action under each thumbnail. */
|
|
17
|
+
redo: string;
|
|
18
|
+
/** The screen reader label for that action, naming the side. */
|
|
19
|
+
redoAccessibility: (sideLabel: string) => string;
|
|
20
|
+
/** The enlarged view's button, naming the side. */
|
|
21
|
+
redoSide: (sideLabel: string) => string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function documentReviewCopy(mode: DocumentCaptureMode, twoSided: boolean): DocumentReviewCopy {
|
|
25
|
+
if (mode === 'upload') {
|
|
26
|
+
return {
|
|
27
|
+
status: twoSided ? 'Both sides added' : 'Photo added',
|
|
28
|
+
redo: 'Replace',
|
|
29
|
+
redoAccessibility: (side) => `Replace the ${side.toLowerCase()} photo`,
|
|
30
|
+
redoSide: (side) => `Replace ${side.toLowerCase()}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
status: twoSided ? 'Both sides captured' : 'Photo captured',
|
|
35
|
+
redo: 'Retake',
|
|
36
|
+
redoAccessibility: (side) => `Retake ${side.toLowerCase()}`,
|
|
37
|
+
redoSide: (side) => `Retake ${side.toLowerCase()}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
package/src/components/fonts.ts
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import type { TextStyle } from 'react-native';
|
|
2
2
|
import { useFonts } from 'expo-font';
|
|
3
|
-
import { SpaceGrotesk_500Medium, SpaceGrotesk_600SemiBold, SpaceGrotesk_700Bold } from '@expo-google-fonts/space-grotesk';
|
|
4
|
-
import { Karla_400Regular, Karla_500Medium, Karla_600SemiBold, Karla_700Bold } from '@expo-google-fonts/karla';
|
|
5
3
|
import { fontFamilyFor } from '../config/font-resolve';
|
|
6
4
|
import { useTheme } from './theme-provider';
|
|
7
5
|
|
|
8
6
|
export { fontFamilyFor, markFamilyName, brandFamilyName, BRAND_WEIGHTS } from '../config/font-resolve';
|
|
9
7
|
|
|
10
|
-
|
|
11
8
|
// Typography fonts — the SAME families the Flutter SDK uses via google_fonts:
|
|
12
9
|
// Space Grotesk for headings, Karla for body. Loaded at runtime through
|
|
13
10
|
// expo-font (already natively linked), so no native rebuild is needed. Each
|
|
14
11
|
// weight is its own RN font family, so we resolve family-by-weight below.
|
|
15
|
-
|
|
12
|
+
//
|
|
13
|
+
// The files are VENDORED under src/assets/fonts and required one by one (see
|
|
14
|
+
// the README there). They used to be named imports from the
|
|
15
|
+
// @expo-google-fonts packages, whose index requires EVERY weight the family
|
|
16
|
+
// ships — and Metro bundles every `require` it can see, used or not — so 19
|
|
17
|
+
// font files reached every integrator's app for the seven faces the SDK draws
|
|
18
|
+
// with. Measured at 1.85 MB on a real integrator's release APK.
|
|
16
19
|
export const MYAZA_FONTS = {
|
|
17
|
-
SpaceGrotesk_500Medium,
|
|
18
|
-
SpaceGrotesk_600SemiBold,
|
|
19
|
-
SpaceGrotesk_700Bold,
|
|
20
|
-
Karla_400Regular,
|
|
21
|
-
Karla_500Medium,
|
|
22
|
-
Karla_600SemiBold,
|
|
23
|
-
Karla_700Bold,
|
|
20
|
+
SpaceGrotesk_500Medium: require('../assets/fonts/SpaceGrotesk_500Medium.ttf'),
|
|
21
|
+
SpaceGrotesk_600SemiBold: require('../assets/fonts/SpaceGrotesk_600SemiBold.ttf'),
|
|
22
|
+
SpaceGrotesk_700Bold: require('../assets/fonts/SpaceGrotesk_700Bold.ttf'),
|
|
23
|
+
Karla_400Regular: require('../assets/fonts/Karla_400Regular.ttf'),
|
|
24
|
+
Karla_500Medium: require('../assets/fonts/Karla_500Medium.ttf'),
|
|
25
|
+
Karla_600SemiBold: require('../assets/fonts/Karla_600SemiBold.ttf'),
|
|
26
|
+
Karla_700Bold: require('../assets/fonts/Karla_700Bold.ttf'),
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
/** Loads the Myaza fonts. Returns true once they're ready (system font until then). */
|
|
@@ -29,12 +32,6 @@ export function useMyazaFonts(): boolean {
|
|
|
29
32
|
return loaded;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
/**
|
|
33
|
-
* Resolves the concrete RN font family for a (heading vs body, weight) pair.
|
|
34
|
-
* Returns `undefined` until fonts are loaded so text falls back to the system
|
|
35
|
-
* font + `fontWeight` without triggering an "unrecognized font" warning.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
35
|
/**
|
|
39
36
|
* The body font family for a TEXT INPUT, at the given weight.
|
|
40
37
|
*
|