@myazahq/kyc-sdk-react-native 2.4.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/build.gradle +35 -3
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +77 -0
- package/app.plugin.js +89 -8
- package/ios/HybridMyazaFaceDetector.swift +16 -3
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
- package/package.json +14 -2
- package/src/MyazaBiometricAuth.tsx +119 -0
- package/src/MyazaKYC.tsx +12 -0
- package/src/components/CountryField.tsx +8 -0
- package/src/components/CountryRegionPicker.tsx +184 -0
- package/src/components/DashedBorder.tsx +55 -0
- package/src/components/DialCodePicker.tsx +53 -67
- 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 -176
- 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/MultiIdProgress.tsx +107 -0
- package/src/components/MyazaInput.tsx +39 -4
- package/src/components/MyazaSelect.tsx +2 -1
- package/src/components/OptionRow.tsx +22 -3
- package/src/components/OwnershipSlider.tsx +102 -0
- package/src/components/PhoneNumberInput.tsx +45 -4
- package/src/components/PoweredBy.tsx +5 -5
- package/src/components/PresenceBlocks.tsx +153 -0
- package/src/components/StaggerIn.tsx +63 -0
- package/src/components/StepView.tsx +43 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/glass/FloatingSheet.tsx +73 -21
- package/src/components/glass/GlassSheet.tsx +14 -2
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +45 -5
- package/src/config/addressCollection.ts +125 -0
- package/src/config/biometricOptions.ts +100 -0
- package/src/config/business.ts +26 -7
- package/src/config/businessDetailsValidity.ts +52 -0
- package/src/config/businessPrefill.ts +72 -0
- package/src/config/businessSteps.ts +20 -3
- package/src/config/consentStep.ts +19 -0
- package/src/config/keyPeople.ts +111 -7
- package/src/config/keyPeopleCorporate.ts +19 -0
- package/src/config/keyPeopleOwnerTypes.ts +12 -0
- package/src/config/keyPeopleOwners.ts +26 -0
- package/src/config/keyPeoplePrefill.ts +138 -0
- package/src/config/keyPeopleSectionDefs.ts +65 -0
- package/src/config/keyPeopleSections.ts +167 -0
- package/src/config/proofOfAddress.ts +54 -11
- package/src/config/regions.ts +24 -0
- package/src/config/registrationHint.ts +1 -1
- package/src/config/stepOrder.ts +100 -6
- package/src/config/theme.ts +8 -2
- package/src/config/uploadLimits.ts +35 -0
- package/src/config/website.ts +36 -0
- package/src/config/workflowMerge.ts +16 -5
- package/src/emrtd/activeAuth.ts +100 -0
- package/src/emrtd/extras.ts +76 -0
- package/src/emrtd/files.ts +5 -0
- package/src/emrtd/index.ts +1 -0
- package/src/emrtd/read.ts +8 -1
- package/src/emrtd/session.ts +41 -0
- package/src/emrtd/stages.ts +11 -0
- 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/contact-recovery.ts +44 -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/multi-id.ts +210 -0
- package/src/lib/poa-country-gate.ts +27 -0
- package/src/lib/resubmit.ts +130 -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/screen-corners.ts +50 -0
- package/src/lib/selfie-upload-wait.ts +79 -0
- package/src/lib/step-log.ts +24 -3
- package/src/lib/street-view-fov.ts +42 -0
- package/src/lib/use-multi-id-plan.ts +29 -0
- package/src/lib/webview-available.ts +47 -0
- package/src/liveness/useLiveness.ts +6 -1
- package/src/liveness/useModelReady.ts +65 -0
- package/src/liveness/visionCameraFaceDetector.ts +35 -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/ApplicantRoleStep.tsx +9 -5
- package/src/screens/BusinessCheckPanel.tsx +89 -0
- package/src/screens/BusinessDetailsFields.tsx +112 -0
- package/src/screens/BusinessDetailsStep.tsx +133 -145
- package/src/screens/BusinessDocumentSlot.tsx +5 -5
- package/src/screens/BusinessDocumentsStep.tsx +5 -56
- package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
- package/src/screens/BusinessPickedCard.tsx +86 -0
- package/src/screens/BusinessPickedSection.tsx +51 -0
- package/src/screens/BusinessRegistryPickers.tsx +67 -0
- package/src/screens/BusinessSandboxToggle.tsx +114 -0
- package/src/screens/BusinessSearch.tsx +199 -0
- package/src/screens/BusinessSearchResults.tsx +115 -0
- package/src/screens/CompanyInfoFields.tsx +91 -51
- package/src/screens/ConsentStep.tsx +37 -17
- package/src/screens/ContactActions.tsx +54 -0
- package/src/screens/ContactDestinationField.tsx +5 -0
- package/src/screens/ContactEntryPanel.tsx +75 -0
- package/src/screens/ContactVerificationStep.tsx +43 -42
- package/src/screens/CountrySelectStep.tsx +13 -147
- package/src/screens/IdTypeStep.tsx +12 -5
- package/src/screens/KeepLinksSheet.tsx +88 -0
- package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
- package/src/screens/KeyPeopleAwaitList.tsx +126 -0
- package/src/screens/KeyPeoplePending.tsx +154 -0
- package/src/screens/KeyPeopleSection.tsx +162 -0
- package/src/screens/KeyPeopleSectionsList.tsx +103 -0
- package/src/screens/KeyPeopleUboExemption.tsx +72 -0
- package/src/screens/KeyPersonCard.tsx +36 -4
- package/src/screens/KeyPersonForm.tsx +195 -36
- package/src/screens/KeyPersonKindToggle.tsx +68 -0
- package/src/screens/KeyPersonOwners.tsx +109 -0
- package/src/screens/KeyPersonRoleChips.tsx +68 -0
- package/src/screens/KeyPersonSheet.tsx +92 -108
- package/src/screens/LivenessAvatar.tsx +15 -3
- package/src/screens/LivenessStep.tsx +172 -52
- package/src/screens/NfcStep.tsx +14 -0
- package/src/screens/PoaDocumentTypeList.tsx +66 -0
- package/src/screens/ProofOfAddressParts.tsx +151 -0
- package/src/screens/ProofOfAddressStep.tsx +58 -96
- 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 +82 -119
- 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/NfcReadProgress.tsx +1 -0
- package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
- package/src/screens/useAddressPhotoAttach.ts +77 -0
- package/src/screens/useAwaitingPeople.ts +90 -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 +251 -3
- package/src/services/api-verify-types.ts +90 -1
- package/src/services/api.ts +237 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/errors.ts +10 -2
- package/src/services/location.ts +234 -0
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +8 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
- package/src/store/address.ts +97 -0
- package/src/store/businessCheck.ts +113 -0
- package/src/store/derive.ts +44 -4
- package/src/store/kycStore.ts +352 -16
- package/src/store/serverConfig.ts +20 -0
- package/src/store/session.ts +227 -0
- package/src/store/state.ts +289 -2
- package/src/store/submit.ts +80 -13
- package/src/store/submitApplicant.ts +5 -11
- package/src/types/business.ts +23 -2
- package/src/types/config.ts +70 -2
- package/src/types/verification.ts +13 -1
- package/src/types/workflow.ts +112 -1
- package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
|
@@ -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,63 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { AccessibilityInfo, Animated } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether the OS asks for reduced motion. Shared by the entrance/skeleton
|
|
6
|
+
* animations so a system-level preference silences all of them at once.
|
|
7
|
+
*/
|
|
8
|
+
export function useReduceMotion(): boolean {
|
|
9
|
+
const [reduced, setReduced] = useState(false);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
let cancelled = false;
|
|
12
|
+
void AccessibilityInfo.isReduceMotionEnabled().then((r) => {
|
|
13
|
+
if (!cancelled) setReduced(r);
|
|
14
|
+
});
|
|
15
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduced);
|
|
16
|
+
return () => {
|
|
17
|
+
cancelled = true;
|
|
18
|
+
sub.remove();
|
|
19
|
+
};
|
|
20
|
+
}, []);
|
|
21
|
+
return reduced;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Rise-and-fade entrance for list items, mirroring the web SDK's staggered
|
|
26
|
+
* `animate-slide-up`: each card enters in reading order, ~45ms apart, so a
|
|
27
|
+
* resolved list reads as the skeleton before it settling into place rather
|
|
28
|
+
* than a screen swap. Reduced motion renders children statically.
|
|
29
|
+
*/
|
|
30
|
+
export function StaggerIn({
|
|
31
|
+
delayMs = 0,
|
|
32
|
+
children,
|
|
33
|
+
}: {
|
|
34
|
+
delayMs?: number;
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
}): React.ReactElement {
|
|
37
|
+
const reduceMotion = useReduceMotion();
|
|
38
|
+
const progress = useRef(new Animated.Value(0)).current;
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
Animated.timing(progress, {
|
|
42
|
+
toValue: 1,
|
|
43
|
+
duration: 300,
|
|
44
|
+
delay: delayMs,
|
|
45
|
+
useNativeDriver: true,
|
|
46
|
+
}).start();
|
|
47
|
+
}, [progress, delayMs]);
|
|
48
|
+
|
|
49
|
+
if (reduceMotion) return <>{children}</>;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Animated.View
|
|
53
|
+
style={{
|
|
54
|
+
opacity: progress,
|
|
55
|
+
transform: [
|
|
56
|
+
{ translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [16, 0] }) },
|
|
57
|
+
],
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{children}
|
|
61
|
+
</Animated.View>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -12,12 +12,20 @@ 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';
|
|
18
24
|
import { BusinessDocumentsStep } from '../screens/BusinessDocumentsStep';
|
|
19
25
|
import { ApplicantRoleStep } from '../screens/ApplicantRoleStep';
|
|
20
26
|
import { NfcStep } from '../screens/NfcStep';
|
|
27
|
+
import { MultiIdProgress } from './MultiIdProgress';
|
|
28
|
+
import { useMultiIdPlan } from '../lib/use-multi-id-plan';
|
|
21
29
|
|
|
22
30
|
// ---------------------------------------------------------------------------
|
|
23
31
|
// The step router — which screen a step renders.
|
|
@@ -26,7 +34,31 @@ import { NfcStep } from '../screens/NfcStep';
|
|
|
26
34
|
// and the step order, and nothing else.
|
|
27
35
|
// ---------------------------------------------------------------------------
|
|
28
36
|
|
|
37
|
+
/** The steps a multi-ID run walks once PER ID — the ones whose screen is about
|
|
38
|
+
* one particular check and therefore needs the position strip above it. */
|
|
39
|
+
const MULTI_ID_STEPS: readonly KYCStep[] = [
|
|
40
|
+
'id-type',
|
|
41
|
+
'id-input',
|
|
42
|
+
'document-capture',
|
|
43
|
+
'nfc',
|
|
44
|
+
'liveness',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** Wraps a step's screen with the multi-ID position strip when a run is active. */
|
|
29
48
|
export function StepView({ step, onClose }: { step: KYCStep; onClose: () => void }): React.ReactElement {
|
|
49
|
+
const plan = useMultiIdPlan();
|
|
50
|
+
if (plan && MULTI_ID_STEPS.includes(step)) {
|
|
51
|
+
return (
|
|
52
|
+
<View style={{ flex: 1 }}>
|
|
53
|
+
<MultiIdProgress plan={plan} />
|
|
54
|
+
<View style={{ flex: 1 }}>{stepScreen(step, onClose)}</View>
|
|
55
|
+
</View>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return stepScreen(step, onClose);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function stepScreen(step: KYCStep, onClose: () => void): React.ReactElement {
|
|
30
62
|
switch (step) {
|
|
31
63
|
case 'consent':
|
|
32
64
|
return <ConsentStep />;
|
|
@@ -52,6 +84,17 @@ export function StepView({ step, onClose }: { step: KYCStep; onClose: () => void
|
|
|
52
84
|
return <ContactVerificationStep key="contact-phone" channel="phone" />;
|
|
53
85
|
case 'proof-of-address':
|
|
54
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 />;
|
|
55
98
|
case 'country-select':
|
|
56
99
|
return <CountrySelectStep />;
|
|
57
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
|
+
}
|
|
@@ -8,9 +8,8 @@ import {
|
|
|
8
8
|
View,
|
|
9
9
|
type ViewStyle,
|
|
10
10
|
} from 'react-native';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
import { radius, spacing } from '../../config/theme';
|
|
11
|
+
import { spacing } from '../../config/theme';
|
|
12
|
+
import { displayCornerRadius } from '../../lib/screen-corners';
|
|
14
13
|
import { useTheme } from '../runtime';
|
|
15
14
|
import { GlassIconButton } from '../GlassIconButton';
|
|
16
15
|
import { GlassSheet } from './GlassSheet';
|
|
@@ -18,10 +17,12 @@ import { GlassSheet } from './GlassSheet';
|
|
|
18
17
|
// ---------------------------------------------------------------------------
|
|
19
18
|
// FloatingSheet — the shell every bottom sheet in the SDK sits in.
|
|
20
19
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
20
|
+
// A floating card: detached from the screen's edges by a TIGHT gap (8px), all
|
|
21
|
+
// four corners rounded, a grab handle centred under a whisper of padding. The
|
|
22
|
+
// gap is deliberately small — the earlier float swam in margin and spent
|
|
23
|
+
// screen height a phone does not have, while welding it flush read as a
|
|
24
|
+
// different surface from the SDK's glass language. The 8px is what says
|
|
25
|
+
// "card above the page" without costing content room.
|
|
25
26
|
//
|
|
26
27
|
// It offers three ways out, because a sheet that can only be dismissed one way
|
|
27
28
|
// is a trap for whichever user does not find that way:
|
|
@@ -34,6 +35,28 @@ import { GlassSheet } from './GlassSheet';
|
|
|
34
35
|
// apart on handle size, dismiss threshold and safe-area maths.
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
36
37
|
|
|
38
|
+
/** Detached from the screen edges by a tight, even gap. */
|
|
39
|
+
const FLOAT_GAP = spacing.sm;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The float's corner radius is PRESENTATION GRAMMAR, not branding — and it is
|
|
43
|
+
* computed the way Apple computes nested corners: CONCENTRIC with the display,
|
|
44
|
+
* i.e. the device's own corner radius minus the gap, so the sheet's curve runs
|
|
45
|
+
* parallel to the phone's. Square-cornered screens have no curve to be
|
|
46
|
+
* concentric with, so they keep the share-sheet look instead. Never the
|
|
47
|
+
* brand-scaled `radius` tokens: an org whose buttons are 4px square should not
|
|
48
|
+
* get a squared-off bottom sheet.
|
|
49
|
+
*/
|
|
50
|
+
const screenRadius = displayCornerRadius();
|
|
51
|
+
const SHEET_RADIUS = screenRadius > 0 ? Math.max(screenRadius - FLOAT_GAP, 16) : 38;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The corner curve eats into the header, so the close button's inset SCALES
|
|
55
|
+
* with the radius — ~0.42·R is where the system sheet sits its own X. A fixed
|
|
56
|
+
* inset that cleared 38pt corners sat ON the curve at a 16-class phone's 47.
|
|
57
|
+
*/
|
|
58
|
+
const CLOSE_INSET = Math.max(14, Math.round(SHEET_RADIUS * 0.42));
|
|
59
|
+
|
|
37
60
|
/** How far down a drag must travel before release dismisses instead of springing back. */
|
|
38
61
|
const DISMISS_DISTANCE = 90;
|
|
39
62
|
/** A flick dismisses at any distance — velocity is intent, distance is only evidence of it. */
|
|
@@ -121,14 +144,16 @@ export function FloatingSheet({
|
|
|
121
144
|
}),
|
|
122
145
|
).current;
|
|
123
146
|
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
147
|
+
// A TIGHT float: detached from the edges by a whisper (8px), not welded to
|
|
148
|
+
// them and not swimming in margin. The gap is what says "card above the
|
|
149
|
+
// page" — any more of it and the sheet loses height it needs for content.
|
|
128
150
|
const panel: ViewStyle = {
|
|
129
|
-
marginHorizontal:
|
|
130
|
-
//
|
|
131
|
-
|
|
151
|
+
marginHorizontal: FLOAT_GAP,
|
|
152
|
+
// The CARD sits low, like the system share sheet: a tight 8px off the
|
|
153
|
+
// screen edge, with the home indicator riding ON the card. Reserving the
|
|
154
|
+
// whole safe-area under it left the sheet hovering mid-air. The CONTENT
|
|
155
|
+
// still clears the indicator — that inset moves INSIDE the card (below).
|
|
156
|
+
marginBottom: bottomOffset > 0 ? bottomOffset + FLOAT_GAP : FLOAT_GAP,
|
|
132
157
|
};
|
|
133
158
|
|
|
134
159
|
return (
|
|
@@ -147,7 +172,18 @@ export function FloatingSheet({
|
|
|
147
172
|
<Animated.View style={[panel, { transform: [{ translateY }] }]}>
|
|
148
173
|
{/* Swallows taps so only the backdrop dismisses. */}
|
|
149
174
|
<Pressable onPress={() => undefined}>
|
|
150
|
-
<GlassSheet
|
|
175
|
+
<GlassSheet
|
|
176
|
+
style={{
|
|
177
|
+
// Floating card: all four corners round, not just the top
|
|
178
|
+
// pair — at the SYSTEM sheet's radius, never the brand scale.
|
|
179
|
+
borderRadius: SHEET_RADIUS,
|
|
180
|
+
maxHeight,
|
|
181
|
+
// No bottom padding, deliberately: a scrolling body runs all
|
|
182
|
+
// the way to the lip and is CLIPPED by the curve — content
|
|
183
|
+
// sliding under the rounded edge, the way the system does it.
|
|
184
|
+
overflow: 'hidden',
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
151
187
|
{/* The drag lives on the header, NOT the whole panel: bodies here
|
|
152
188
|
scroll, and a panel-wide responder fights the list for every
|
|
153
189
|
downward swipe. */}
|
|
@@ -155,9 +191,15 @@ export function FloatingSheet({
|
|
|
155
191
|
{...pan.panHandlers}
|
|
156
192
|
style={{
|
|
157
193
|
flexDirection: 'row',
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
194
|
+
// TOP, not centre. Centred, the handle sits against the
|
|
195
|
+
// tallest child, so the close button's height decided how far
|
|
196
|
+
// down the panel the grabber sat. Each now hangs off the top
|
|
197
|
+
// with its own offset, which is what puts the handle at the
|
|
198
|
+
// lip where a grabber belongs. Matches the Flutter sheet.
|
|
199
|
+
alignItems: 'flex-start',
|
|
200
|
+
// Radius-proportional (CLOSE_INSET): deep concentric corners
|
|
201
|
+
// need the button further from the edges than shallow ones.
|
|
202
|
+
paddingHorizontal: CLOSE_INSET,
|
|
161
203
|
}}
|
|
162
204
|
>
|
|
163
205
|
<View style={{ flex: 1 }} />
|
|
@@ -165,12 +207,22 @@ export function FloatingSheet({
|
|
|
165
207
|
<View
|
|
166
208
|
style={{
|
|
167
209
|
width: 36,
|
|
168
|
-
height:
|
|
169
|
-
borderRadius: 2,
|
|
210
|
+
height: 5,
|
|
211
|
+
borderRadius: 2.5,
|
|
212
|
+
marginTop: 8,
|
|
170
213
|
backgroundColor: colors.border,
|
|
171
214
|
}}
|
|
172
215
|
/>
|
|
173
|
-
|
|
216
|
+
{/* Pushed down as well as in: the button's own box already
|
|
217
|
+
holds the icon off its edges, so set flush to the top it
|
|
218
|
+
hung off the corner instead of tucking into it. */}
|
|
219
|
+
<View
|
|
220
|
+
style={{
|
|
221
|
+
flex: 1,
|
|
222
|
+
alignItems: 'flex-end',
|
|
223
|
+
marginTop: Math.min(16, Math.max(6, CLOSE_INSET - 6)),
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
174
226
|
<GlassIconButton
|
|
175
227
|
icon="close"
|
|
176
228
|
onPress={onClose}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
import { StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { useTheme } from '../runtime';
|
|
5
5
|
import { GlassSurface } from './GlassSurface';
|
|
@@ -29,9 +29,21 @@ export interface GlassSheetProps {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function GlassSheet({ children, style }: GlassSheetProps): React.ReactElement {
|
|
32
|
-
const { colors } = useTheme();
|
|
32
|
+
const { colors, mode } = useTheme();
|
|
33
33
|
return (
|
|
34
34
|
<GlassSurface glassStyle="regular" fallbackColor={colors.background} style={style}>
|
|
35
|
+
{/* A WHISPER of scrim between the glass and the content — the sheet
|
|
36
|
+
must stay glass (the flow showing through is the point). This only
|
|
37
|
+
takes the edge off a strongly tinted flow so body text keeps
|
|
38
|
+
contrast; anything critical sits on its own alert panel besides.
|
|
39
|
+
Slightly heavier in light mode, where the wash was worst. */}
|
|
40
|
+
<View
|
|
41
|
+
pointerEvents="none"
|
|
42
|
+
style={[
|
|
43
|
+
StyleSheet.absoluteFill,
|
|
44
|
+
{ backgroundColor: colors.background, opacity: mode === 'light' ? 0.35 : 0.18 },
|
|
45
|
+
]}
|
|
46
|
+
/>
|
|
35
47
|
{children}
|
|
36
48
|
</GlassSurface>
|
|
37
49
|
);
|