@myazahq/kyc-sdk-react-native 2.3.0 → 2.5.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/android/build.gradle +35 -3
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +75 -0
- package/ios/HybridMyazaFaceDetector.swift +8 -0
- 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 +1 -2
- package/src/MyazaKYC.tsx +12 -0
- package/src/components/BrandBar.tsx +137 -0
- package/src/components/DashedBorder.tsx +55 -0
- package/src/components/DialCodePicker.tsx +25 -24
- package/src/components/DocumentReviewSide.tsx +34 -14
- package/src/components/Icon.tsx +14 -0
- package/src/components/KycSheet.tsx +39 -140
- package/src/components/MediaSourceSheet.tsx +7 -37
- package/src/components/MultiIdProgress.tsx +107 -0
- package/src/components/MyazaDateField.tsx +6 -20
- package/src/components/MyazaInput.tsx +23 -4
- package/src/components/MyazaSelect.tsx +17 -40
- package/src/components/OwnershipSlider.tsx +102 -0
- package/src/components/PhoneNumberInput.tsx +37 -4
- package/src/components/SandboxBanner.tsx +92 -0
- package/src/components/StaggerIn.tsx +63 -0
- package/src/components/StepHeader.tsx +15 -2
- package/src/components/StepView.tsx +26 -0
- package/src/components/glass/ChromeGlass.tsx +65 -0
- package/src/components/glass/FloatingSheet.tsx +242 -0
- package/src/components/glass/GlassSheet.tsx +50 -0
- package/src/components/glass/GlassSurface.tsx +31 -3
- package/src/components/stepHeaderMeta.tsx +3 -1
- package/src/components/viewfinder/ImmersiveBottomBar.tsx +28 -17
- package/src/components/viewfinder/ImmersiveControls.tsx +26 -14
- package/src/components/viewfinder/ViewfinderControls.tsx +29 -7
- package/src/config/business.ts +6 -6
- package/src/config/businessDetailsValidity.ts +52 -0
- package/src/config/businessPrefill.ts +72 -0
- package/src/config/businessSteps.ts +14 -3
- 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/registrationHint.ts +1 -1
- package/src/config/stepOrder.ts +24 -4
- package/src/config/theme.ts +8 -2
- package/src/config/website.ts +36 -0
- package/src/config/workflowMerge.ts +5 -0
- 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/lib/contact-recovery.ts +44 -0
- package/src/lib/multi-id.ts +210 -0
- package/src/lib/resubmit.ts +130 -0
- package/src/lib/screen-corners.ts +50 -0
- package/src/lib/step-log.ts +24 -3
- package/src/lib/use-multi-id-plan.ts +29 -0
- package/src/liveness/useModelReady.ts +65 -0
- package/src/liveness/visionCameraFaceDetector.ts +35 -0
- package/src/screens/ApplicantRoleStep.tsx +9 -5
- package/src/screens/BusinessCheckPanel.tsx +89 -0
- package/src/screens/BusinessDetailsFields.tsx +109 -0
- package/src/screens/BusinessDetailsStep.tsx +131 -145
- package/src/screens/BusinessDocumentSlot.tsx +2 -3
- package/src/screens/BusinessDocumentsStep.tsx +1 -1
- 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 +86 -51
- package/src/screens/ConsentStep.tsx +31 -0
- package/src/screens/ContactDestinationField.tsx +1 -0
- package/src/screens/ContactVerificationStep.tsx +34 -3
- 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/LivenessStep.tsx +18 -0
- package/src/screens/NfcStep.tsx +14 -0
- package/src/screens/ProofOfAddressStep.tsx +6 -3
- package/src/screens/SubmittedStep.tsx +66 -10
- package/src/screens/nfc/NfcReadProgress.tsx +1 -0
- package/src/screens/useAwaitingPeople.ts +90 -0
- package/src/services/api-types.ts +158 -2
- package/src/services/api-verify-types.ts +53 -1
- package/src/services/api.ts +104 -0
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/errors.ts +10 -2
- package/src/services/workflowGate.ts +1 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
- package/src/store/businessCheck.ts +113 -0
- package/src/store/derive.ts +15 -0
- package/src/store/kycStore.ts +241 -14
- package/src/store/serverConfig.ts +2 -0
- package/src/store/session.ts +208 -0
- package/src/store/state.ts +155 -2
- package/src/store/submit.ts +63 -13
- package/src/store/submitApplicant.ts +5 -11
- package/src/types/business.ts +23 -2
- package/src/types/config.ts +29 -3
- package/src/types/verification.ts +1 -1
- package/src/types/workflow.ts +15 -0
- package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
|
@@ -12,7 +12,10 @@ import { CountryFlag } from './CountryFlag';
|
|
|
12
12
|
// title (heading2) + optional country flag, then an optional description below
|
|
13
13
|
// (indented to align under the title when a back button is present).
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// Back button width + gap. Kept in step with BACK_SIZE below: it indents the
|
|
16
|
+
// description so it starts under the title rather than under the button.
|
|
17
|
+
const BACK_SIZE = 40;
|
|
18
|
+
const BACK_FOOTPRINT = BACK_SIZE + spacing.sm;
|
|
16
19
|
|
|
17
20
|
export interface StepHeaderProps {
|
|
18
21
|
title: string;
|
|
@@ -28,7 +31,17 @@ export function StepHeader({ title, description, onBack, country }: StepHeaderPr
|
|
|
28
31
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
29
32
|
{onBack ? (
|
|
30
33
|
<View style={{ marginRight: spacing.sm }}>
|
|
31
|
-
|
|
34
|
+
{/* Same 40pt footprint as the theme/close chrome above it — a
|
|
35
|
+
smaller glass circle here read as a different class of control,
|
|
36
|
+
and 28pt was under the 44pt minimum touch target even with
|
|
37
|
+
hitSlop. */}
|
|
38
|
+
<GlassIconButton
|
|
39
|
+
icon="back"
|
|
40
|
+
onPress={onBack}
|
|
41
|
+
size={BACK_SIZE}
|
|
42
|
+
iconSize={22}
|
|
43
|
+
accessibilityLabel="Back"
|
|
44
|
+
/>
|
|
32
45
|
</View>
|
|
33
46
|
) : null}
|
|
34
47
|
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
|
|
@@ -18,6 +18,8 @@ import { BusinessKeyPeopleStep } from '../screens/BusinessKeyPeopleStep';
|
|
|
18
18
|
import { BusinessDocumentsStep } from '../screens/BusinessDocumentsStep';
|
|
19
19
|
import { ApplicantRoleStep } from '../screens/ApplicantRoleStep';
|
|
20
20
|
import { NfcStep } from '../screens/NfcStep';
|
|
21
|
+
import { MultiIdProgress } from './MultiIdProgress';
|
|
22
|
+
import { useMultiIdPlan } from '../lib/use-multi-id-plan';
|
|
21
23
|
|
|
22
24
|
// ---------------------------------------------------------------------------
|
|
23
25
|
// The step router — which screen a step renders.
|
|
@@ -26,7 +28,31 @@ import { NfcStep } from '../screens/NfcStep';
|
|
|
26
28
|
// and the step order, and nothing else.
|
|
27
29
|
// ---------------------------------------------------------------------------
|
|
28
30
|
|
|
31
|
+
/** The steps a multi-ID run walks once PER ID — the ones whose screen is about
|
|
32
|
+
* one particular check and therefore needs the position strip above it. */
|
|
33
|
+
const MULTI_ID_STEPS: readonly KYCStep[] = [
|
|
34
|
+
'id-type',
|
|
35
|
+
'id-input',
|
|
36
|
+
'document-capture',
|
|
37
|
+
'nfc',
|
|
38
|
+
'liveness',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/** Wraps a step's screen with the multi-ID position strip when a run is active. */
|
|
29
42
|
export function StepView({ step, onClose }: { step: KYCStep; onClose: () => void }): React.ReactElement {
|
|
43
|
+
const plan = useMultiIdPlan();
|
|
44
|
+
if (plan && MULTI_ID_STEPS.includes(step)) {
|
|
45
|
+
return (
|
|
46
|
+
<View style={{ flex: 1 }}>
|
|
47
|
+
<MultiIdProgress plan={plan} />
|
|
48
|
+
<View style={{ flex: 1 }}>{stepScreen(step, onClose)}</View>
|
|
49
|
+
</View>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return stepScreen(step, onClose);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stepScreen(step: KYCStep, onClose: () => void): React.ReactElement {
|
|
30
56
|
switch (step) {
|
|
31
57
|
case 'consent':
|
|
32
58
|
return <ConsentStep />;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { GlassSurface } from './GlassSurface';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// ChromeGlass — the surface behind controls that float over the LIVE CAMERA.
|
|
8
|
+
//
|
|
9
|
+
// This is the strongest case for Liquid Glass in the SDK: the chrome floats
|
|
10
|
+
// above content, it is interactive, and the background is a moving scene rather
|
|
11
|
+
// than a flat tint, which is exactly the condition the material was designed
|
|
12
|
+
// for (it is what the iOS Camera app does).
|
|
13
|
+
//
|
|
14
|
+
// Two properties are load-bearing, and both exist to protect the white glyphs:
|
|
15
|
+
//
|
|
16
|
+
// • `colorScheme: 'dark'` — forced, NOT the app theme. These icons are white
|
|
17
|
+
// against a camera feed, not against the app's background, so a light app
|
|
18
|
+
// theme would put pale glass under white icons.
|
|
19
|
+
//
|
|
20
|
+
// • `tintColor` — a dark bias over the glass. Plain glass is translucent, so
|
|
21
|
+
// a white passport page filling the frame would wash a white glyph out.
|
|
22
|
+
// The flat scrim this replaces guaranteed contrast; the tint is what buys
|
|
23
|
+
// that guarantee back while keeping the material's depth and adaptivity.
|
|
24
|
+
//
|
|
25
|
+
// Off iOS 26 it renders the ORIGINAL flat scrim, so Android and older iOS are
|
|
26
|
+
// pixel-identical to before. Glass stays purely additive.
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/** The flat scrim used before glass, and still the fallback everywhere else. */
|
|
30
|
+
export const CHROME_SCRIM = 'rgba(0,0,0,0.55)';
|
|
31
|
+
|
|
32
|
+
/** Dark bias laid over the glass so white glyphs survive a bright scene. */
|
|
33
|
+
const CHROME_TINT = 'rgba(0,0,0,0.28)';
|
|
34
|
+
|
|
35
|
+
export interface ChromeGlassProps {
|
|
36
|
+
children?: React.ReactNode;
|
|
37
|
+
style?: StyleProp<ViewStyle>;
|
|
38
|
+
/** Whether the glass reacts to touch. True for buttons, false for pills. */
|
|
39
|
+
interactive?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Overrides the scrim on the fallback path. Pass the value the call site used
|
|
42
|
+
* before, so non-glass devices keep their exact previous appearance.
|
|
43
|
+
*/
|
|
44
|
+
scrim?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ChromeGlass({
|
|
48
|
+
children,
|
|
49
|
+
style,
|
|
50
|
+
interactive = false,
|
|
51
|
+
scrim = CHROME_SCRIM,
|
|
52
|
+
}: ChromeGlassProps): React.ReactElement {
|
|
53
|
+
return (
|
|
54
|
+
<GlassSurface
|
|
55
|
+
glassStyle="regular"
|
|
56
|
+
colorScheme="dark"
|
|
57
|
+
tintColor={CHROME_TINT}
|
|
58
|
+
interactive={interactive}
|
|
59
|
+
fallbackColor={scrim}
|
|
60
|
+
style={style}
|
|
61
|
+
>
|
|
62
|
+
{children}
|
|
63
|
+
</GlassSurface>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Easing,
|
|
5
|
+
Modal,
|
|
6
|
+
PanResponder,
|
|
7
|
+
Pressable,
|
|
8
|
+
View,
|
|
9
|
+
type ViewStyle,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
import { spacing } from '../../config/theme';
|
|
12
|
+
import { displayCornerRadius } from '../../lib/screen-corners';
|
|
13
|
+
import { useTheme } from '../runtime';
|
|
14
|
+
import { GlassIconButton } from '../GlassIconButton';
|
|
15
|
+
import { GlassSheet } from './GlassSheet';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// FloatingSheet — the shell every bottom sheet in the SDK sits in.
|
|
19
|
+
//
|
|
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.
|
|
26
|
+
//
|
|
27
|
+
// It offers three ways out, because a sheet that can only be dismissed one way
|
|
28
|
+
// is a trap for whichever user does not find that way:
|
|
29
|
+
// • swipe it down,
|
|
30
|
+
// • the close button,
|
|
31
|
+
// • tap the backdrop.
|
|
32
|
+
//
|
|
33
|
+
// Shared rather than copied into each sheet: swipe-to-dismiss is the kind of
|
|
34
|
+
// thing that rots when it exists in four places, and the sheets would drift
|
|
35
|
+
// apart on handle size, dismiss threshold and safe-area maths.
|
|
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
|
+
|
|
60
|
+
/** How far down a drag must travel before release dismisses instead of springing back. */
|
|
61
|
+
const DISMISS_DISTANCE = 90;
|
|
62
|
+
/** A flick dismisses at any distance — velocity is intent, distance is only evidence of it. */
|
|
63
|
+
const DISMISS_VELOCITY = 0.7;
|
|
64
|
+
|
|
65
|
+
export interface FloatingSheetProps {
|
|
66
|
+
visible: boolean;
|
|
67
|
+
onClose: () => void;
|
|
68
|
+
children?: React.ReactNode;
|
|
69
|
+
/** Extra bottom offset, e.g. a raised keyboard. */
|
|
70
|
+
bottomOffset?: number;
|
|
71
|
+
/** Caps the panel; the body owns its own scrolling. */
|
|
72
|
+
maxHeight?: number;
|
|
73
|
+
/** Accessible label for the close button. */
|
|
74
|
+
closeLabel?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Fires once the modal has finished going away (iOS only, from `Modal`).
|
|
77
|
+
*
|
|
78
|
+
* Needed by callers that must wait for THIS sheet to be gone before opening
|
|
79
|
+
* another one — iOS refuses to present a second modal while the first is
|
|
80
|
+
* still on screen.
|
|
81
|
+
*/
|
|
82
|
+
onDismiss?: () => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function FloatingSheet({
|
|
86
|
+
visible,
|
|
87
|
+
onClose,
|
|
88
|
+
children,
|
|
89
|
+
bottomOffset = 0,
|
|
90
|
+
maxHeight,
|
|
91
|
+
closeLabel = 'Close',
|
|
92
|
+
onDismiss,
|
|
93
|
+
}: FloatingSheetProps): React.ReactElement {
|
|
94
|
+
const { colors } = useTheme();
|
|
95
|
+
const translateY = useRef(new Animated.Value(0)).current;
|
|
96
|
+
|
|
97
|
+
// The PanResponder is built once, so it would otherwise close over the FIRST
|
|
98
|
+
// onClose forever and stop dismissing after the parent re-renders.
|
|
99
|
+
const onCloseRef = useRef(onClose);
|
|
100
|
+
onCloseRef.current = onClose;
|
|
101
|
+
|
|
102
|
+
// A sheet reopened after being dragged away must not still be off-screen.
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (visible) translateY.setValue(0);
|
|
105
|
+
}, [visible, translateY]);
|
|
106
|
+
|
|
107
|
+
const pan = useRef(
|
|
108
|
+
PanResponder.create({
|
|
109
|
+
// Claim on touch START, not just on move.
|
|
110
|
+
//
|
|
111
|
+
// The panel is wrapped in a Pressable to swallow backdrop taps, and a
|
|
112
|
+
// Pressable takes the responder the instant a finger lands. Asking only
|
|
113
|
+
// on move meant that Pressable had already won and the drag never began.
|
|
114
|
+
// `onStartShouldSetResponder` bubbles from the deepest view up, and this
|
|
115
|
+
// header is deeper than that wrapper, so claiming here beats it — while
|
|
116
|
+
// the close button, deeper still, keeps winning its own taps.
|
|
117
|
+
onStartShouldSetPanResponder: () => true,
|
|
118
|
+
// Kept for the case where a touch begins as something else and turns
|
|
119
|
+
// into a drag. Downward only: stealing horizontal or upward gestures
|
|
120
|
+
// would make the scrollable bodies below feel broken.
|
|
121
|
+
onMoveShouldSetPanResponder: (_, g) => g.dy > 4 && Math.abs(g.dy) > Math.abs(g.dx),
|
|
122
|
+
// Once the drag owns the gesture, nothing may take it mid-pull.
|
|
123
|
+
onPanResponderTerminationRequest: () => false,
|
|
124
|
+
onPanResponderMove: (_, g) => {
|
|
125
|
+
// Downward only: dragging up must not detach the sheet from its corner.
|
|
126
|
+
if (g.dy > 0) translateY.setValue(g.dy);
|
|
127
|
+
},
|
|
128
|
+
onPanResponderRelease: (_, g) => {
|
|
129
|
+
if (g.dy > DISMISS_DISTANCE || g.vy > DISMISS_VELOCITY) {
|
|
130
|
+
Animated.timing(translateY, {
|
|
131
|
+
toValue: 700,
|
|
132
|
+
duration: 180,
|
|
133
|
+
easing: Easing.in(Easing.cubic),
|
|
134
|
+
useNativeDriver: true,
|
|
135
|
+
}).start(() => onCloseRef.current());
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
Animated.spring(translateY, {
|
|
139
|
+
toValue: 0,
|
|
140
|
+
useNativeDriver: true,
|
|
141
|
+
bounciness: 0,
|
|
142
|
+
}).start();
|
|
143
|
+
},
|
|
144
|
+
}),
|
|
145
|
+
).current;
|
|
146
|
+
|
|
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.
|
|
150
|
+
const panel: ViewStyle = {
|
|
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,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<Modal
|
|
161
|
+
visible={visible}
|
|
162
|
+
transparent
|
|
163
|
+
animationType="slide"
|
|
164
|
+
onRequestClose={onClose}
|
|
165
|
+
onDismiss={onDismiss}
|
|
166
|
+
>
|
|
167
|
+
<Pressable
|
|
168
|
+
onPress={onClose}
|
|
169
|
+
accessibilityLabel="Dismiss"
|
|
170
|
+
style={{ flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(0,0,0,0.5)' }}
|
|
171
|
+
>
|
|
172
|
+
<Animated.View style={[panel, { transform: [{ translateY }] }]}>
|
|
173
|
+
{/* Swallows taps so only the backdrop dismisses. */}
|
|
174
|
+
<Pressable onPress={() => undefined}>
|
|
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
|
+
>
|
|
187
|
+
{/* The drag lives on the header, NOT the whole panel: bodies here
|
|
188
|
+
scroll, and a panel-wide responder fights the list for every
|
|
189
|
+
downward swipe. */}
|
|
190
|
+
<View
|
|
191
|
+
{...pan.panHandlers}
|
|
192
|
+
style={{
|
|
193
|
+
flexDirection: 'row',
|
|
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,
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
<View style={{ flex: 1 }} />
|
|
206
|
+
{/* Grab handle — says "draggable" without a caption. */}
|
|
207
|
+
<View
|
|
208
|
+
style={{
|
|
209
|
+
width: 36,
|
|
210
|
+
height: 5,
|
|
211
|
+
borderRadius: 2.5,
|
|
212
|
+
marginTop: 8,
|
|
213
|
+
backgroundColor: colors.border,
|
|
214
|
+
}}
|
|
215
|
+
/>
|
|
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
|
+
>
|
|
226
|
+
<GlassIconButton
|
|
227
|
+
icon="close"
|
|
228
|
+
onPress={onClose}
|
|
229
|
+
size={32}
|
|
230
|
+
iconSize={18}
|
|
231
|
+
accessibilityLabel={closeLabel}
|
|
232
|
+
/>
|
|
233
|
+
</View>
|
|
234
|
+
</View>
|
|
235
|
+
{children}
|
|
236
|
+
</GlassSheet>
|
|
237
|
+
</Pressable>
|
|
238
|
+
</Animated.View>
|
|
239
|
+
</Pressable>
|
|
240
|
+
</Modal>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { useTheme } from '../runtime';
|
|
5
|
+
import { GlassSurface } from './GlassSurface';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// GlassSheet — the panel of a bottom sheet that floats above the flow.
|
|
9
|
+
//
|
|
10
|
+
// A sheet is a surface, not a control, which is the one case where glass is
|
|
11
|
+
// right for something non-interactive: it is a layer that has risen above the
|
|
12
|
+
// content, and letting that content show through is what says so.
|
|
13
|
+
//
|
|
14
|
+
// `glassStyle: 'regular'` rather than `'clear'` on purpose. These panels are
|
|
15
|
+
// full of list text that has to stay readable, and `regular` is the frosted
|
|
16
|
+
// variant; `clear` is for chrome over imagery, where there is little to read.
|
|
17
|
+
//
|
|
18
|
+
// The colour scheme follows the APP theme (unlike the camera chrome, which is
|
|
19
|
+
// pinned dark) because a sheet sits over the SDK's own background, so its text
|
|
20
|
+
// is themed text on a themed surface.
|
|
21
|
+
//
|
|
22
|
+
// Off iOS 26 it is the opaque themed panel it has always been.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export interface GlassSheetProps {
|
|
26
|
+
children?: React.ReactNode;
|
|
27
|
+
/** Panel geometry — corner radii and padding. */
|
|
28
|
+
style?: StyleProp<ViewStyle>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function GlassSheet({ children, style }: GlassSheetProps): React.ReactElement {
|
|
32
|
+
const { colors, mode } = useTheme();
|
|
33
|
+
return (
|
|
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
|
+
/>
|
|
47
|
+
{children}
|
|
48
|
+
</GlassSurface>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Platform, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
GlassView,
|
|
5
|
+
isLiquidGlassAvailable,
|
|
6
|
+
type GlassColorScheme,
|
|
7
|
+
type GlassStyle,
|
|
8
|
+
} from 'expo-glass-effect';
|
|
4
9
|
|
|
5
10
|
import { useTheme } from '../runtime';
|
|
6
11
|
|
|
@@ -10,7 +15,21 @@ import { useTheme } from '../runtime';
|
|
|
10
15
|
// On iOS 26+ (where `isLiquidGlassAvailable()` is true) it renders a native
|
|
11
16
|
// `GlassView`; everywhere else (Android, iOS < 26) it falls back to a plain
|
|
12
17
|
// token-styled surface. Glass is purely additive — every screen looks correct
|
|
13
|
-
// without it.
|
|
18
|
+
// without it.
|
|
19
|
+
//
|
|
20
|
+
// Where it is used, and the rule behind that: glass is for things that FLOAT
|
|
21
|
+
// above content — surfaces (the sheet shell + header, bottom sheets via
|
|
22
|
+
// `GlassSheet`) and interactive chrome (the header icon buttons, the camera
|
|
23
|
+
// controls via `ChromeGlass`).
|
|
24
|
+
//
|
|
25
|
+
// Deliberately NOT used for:
|
|
26
|
+
// • the primary CTA — it needs a solid brand fill to read as the one clear
|
|
27
|
+
// action, and a translucent one competes with everything behind it;
|
|
28
|
+
// • labels and status pills (the brand bar, capture hints, the sandbox
|
|
29
|
+
// strip) — at capsule scale glass reads as a control, so wrapping static
|
|
30
|
+
// text in it promises a tap that does nothing;
|
|
31
|
+
// • any "on" state (torch lit, toggle active) — a material that samples
|
|
32
|
+
// what is behind it cannot state a binary unambiguously.
|
|
14
33
|
// ---------------------------------------------------------------------------
|
|
15
34
|
|
|
16
35
|
const liquidGlass = Platform.OS === 'ios' && isLiquidGlassAvailable();
|
|
@@ -34,6 +53,14 @@ export interface GlassSurfaceProps {
|
|
|
34
53
|
* theme's elevated surface (`backgroundSecondary`).
|
|
35
54
|
*/
|
|
36
55
|
fallbackColor?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Forces the glass to render light or dark, overriding the app theme.
|
|
58
|
+
*
|
|
59
|
+
* Chrome that floats over the CAMERA needs this: its glyphs are white against
|
|
60
|
+
* a live scene, not against the app's background, so following a light app
|
|
61
|
+
* theme would render pale glass under white icons.
|
|
62
|
+
*/
|
|
63
|
+
colorScheme?: GlassColorScheme;
|
|
37
64
|
}
|
|
38
65
|
|
|
39
66
|
export function GlassSurface({
|
|
@@ -43,6 +70,7 @@ export function GlassSurface({
|
|
|
43
70
|
tintColor,
|
|
44
71
|
interactive = false,
|
|
45
72
|
fallbackColor,
|
|
73
|
+
colorScheme,
|
|
46
74
|
}: GlassSurfaceProps): React.ReactElement {
|
|
47
75
|
const { colors, mode } = useTheme();
|
|
48
76
|
|
|
@@ -53,7 +81,7 @@ export function GlassSurface({
|
|
|
53
81
|
glassEffectStyle={glassStyle}
|
|
54
82
|
tintColor={tintColor}
|
|
55
83
|
isInteractive={interactive}
|
|
56
|
-
colorScheme={mode}
|
|
84
|
+
colorScheme={colorScheme ?? mode}
|
|
57
85
|
>
|
|
58
86
|
{children}
|
|
59
87
|
</GlassView>
|
|
@@ -47,7 +47,9 @@ export function stepHeaderMeta(
|
|
|
47
47
|
case 'id-type':
|
|
48
48
|
return { title: 'Select ID Type', description: "Choose the type of identification document you'd like to use." };
|
|
49
49
|
case 'id-input':
|
|
50
|
-
|
|
50
|
+
// The number is all this step asks for. The name comes from the
|
|
51
|
+
// integrator (mount props / the session), never the applicant.
|
|
52
|
+
return { title: `Enter your ${label}`, description: 'We’ll check this against the official record.' };
|
|
51
53
|
case 'document-capture':
|
|
52
54
|
// Phase-aware title/description live in the header (synced from the
|
|
53
55
|
// capture screen via `documentCapturePhase`) — mirrors Flutter.
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { ActivityIndicator, Pressable, View } from 'react-native';
|
|
2
|
+
import { ActivityIndicator, Pressable, View, type ViewStyle } from 'react-native';
|
|
3
3
|
import Svg, { Circle } from 'react-native-svg';
|
|
4
4
|
|
|
5
5
|
import { radius, spacing } from '../../config/theme';
|
|
6
6
|
import { useTheme } from '../runtime';
|
|
7
7
|
import { MyazaText } from '../Typography';
|
|
8
8
|
import { Icon } from '../Icon';
|
|
9
|
+
import { CHROME_SCRIM, ChromeGlass } from '../glass/ChromeGlass';
|
|
9
10
|
|
|
10
|
-
const SCRIM =
|
|
11
|
+
const SCRIM = CHROME_SCRIM;
|
|
12
|
+
|
|
13
|
+
/** The torch's own scrim, kept as-is so non-glass devices look unchanged. */
|
|
14
|
+
const TORCH_SCRIM = 'rgba(0,0,0,0.45)';
|
|
15
|
+
|
|
16
|
+
/** Fills a sized Pressable with a round surface. */
|
|
17
|
+
const ROUND_FILL: ViewStyle = {
|
|
18
|
+
flex: 1,
|
|
19
|
+
borderRadius: radius.full,
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
justifyContent: 'center',
|
|
22
|
+
};
|
|
11
23
|
|
|
12
24
|
/**
|
|
13
25
|
* Fixed slots either side of the shutter.
|
|
@@ -147,22 +159,21 @@ export function BottomBar({
|
|
|
147
159
|
onPress={onToggleTorch}
|
|
148
160
|
accessibilityRole="button"
|
|
149
161
|
accessibilityLabel={torch ? 'Turn off the torch' : 'Turn on the torch'}
|
|
150
|
-
style={{
|
|
151
|
-
width: 36,
|
|
152
|
-
height: 36,
|
|
153
|
-
borderRadius: radius.full,
|
|
154
|
-
// Inverted when on, so "lit" reads at a glance rather than
|
|
155
|
-
// needing the icon to be decoded.
|
|
156
|
-
backgroundColor: torch ? '#FFFFFF' : 'rgba(0,0,0,0.45)',
|
|
157
|
-
alignItems: 'center',
|
|
158
|
-
justifyContent: 'center',
|
|
159
|
-
}}
|
|
162
|
+
style={{ width: 36, height: 36 }}
|
|
160
163
|
>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
{torch ? (
|
|
165
|
+
// Inverted when on, so "lit" reads at a glance rather than
|
|
166
|
+
// needing the icon to be decoded. Solid rather than glass: a
|
|
167
|
+
// translucent surface samples the scene behind it, so "on"
|
|
168
|
+
// would look different depending on where the camera points.
|
|
169
|
+
<View style={[ROUND_FILL, { backgroundColor: '#FFFFFF' }]}>
|
|
170
|
+
<Icon name="zap" size={18} color={colors.primary} />
|
|
171
|
+
</View>
|
|
172
|
+
) : (
|
|
173
|
+
<ChromeGlass interactive scrim={TORCH_SCRIM} style={ROUND_FILL}>
|
|
174
|
+
<Icon name="zap-off" size={18} color="#FFFFFF" />
|
|
175
|
+
</ChromeGlass>
|
|
176
|
+
)}
|
|
166
177
|
</Pressable>
|
|
167
178
|
) : null}
|
|
168
179
|
</View>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { ActivityIndicator, Pressable, View } from 'react-native';
|
|
2
|
+
import { ActivityIndicator, Pressable, View, type ViewStyle } from 'react-native';
|
|
3
3
|
import Svg, { Circle } from 'react-native-svg';
|
|
4
4
|
|
|
5
5
|
import { radius, spacing } from '../../config/theme';
|
|
@@ -7,6 +7,7 @@ import { useTheme } from '../runtime';
|
|
|
7
7
|
import { MyazaText } from '../Typography';
|
|
8
8
|
import { Icon } from '../Icon';
|
|
9
9
|
import { CountryFlag } from '../CountryFlag';
|
|
10
|
+
import { CHROME_SCRIM, ChromeGlass } from '../glass/ChromeGlass';
|
|
10
11
|
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Immersive capture chrome — 1:1 with the Flutter SDK's full-screen camera.
|
|
@@ -18,9 +19,16 @@ import { CountryFlag } from '../CountryFlag';
|
|
|
18
19
|
// • the side badge and document pill sit top, out of the frame's way.
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
|
|
21
|
-
const SCRIM =
|
|
22
|
+
const SCRIM = CHROME_SCRIM;
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Round translucent control — back, torch.
|
|
26
|
+
*
|
|
27
|
+
* Liquid Glass when idle; SOLID brand fill when `active`. An "on" state has to
|
|
28
|
+
* be unmistakable, and a translucent material that samples the scene behind it
|
|
29
|
+
* cannot promise that — the torch would look on or off depending on what the
|
|
30
|
+
* camera happened to be pointed at.
|
|
31
|
+
*/
|
|
24
32
|
export function RoundControl({
|
|
25
33
|
icon,
|
|
26
34
|
label,
|
|
@@ -37,23 +45,27 @@ export function RoundControl({
|
|
|
37
45
|
style: object;
|
|
38
46
|
}): React.ReactElement {
|
|
39
47
|
const { colors } = useTheme();
|
|
48
|
+
const fill: ViewStyle = {
|
|
49
|
+
flex: 1,
|
|
50
|
+
borderRadius: radius.full,
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
justifyContent: 'center',
|
|
53
|
+
};
|
|
54
|
+
const glyph = <Icon name={icon} size={20} color="#FFFFFF" />;
|
|
40
55
|
return (
|
|
41
56
|
<Pressable
|
|
42
57
|
onPress={onPress}
|
|
43
58
|
accessibilityRole="button"
|
|
44
59
|
accessibilityLabel={label}
|
|
45
|
-
style={{
|
|
46
|
-
position: 'absolute',
|
|
47
|
-
width: size,
|
|
48
|
-
height: size,
|
|
49
|
-
borderRadius: radius.full,
|
|
50
|
-
backgroundColor: active ? colors.primary : SCRIM,
|
|
51
|
-
alignItems: 'center',
|
|
52
|
-
justifyContent: 'center',
|
|
53
|
-
...style,
|
|
54
|
-
}}
|
|
60
|
+
style={{ position: 'absolute', width: size, height: size, ...style }}
|
|
55
61
|
>
|
|
56
|
-
|
|
62
|
+
{active ? (
|
|
63
|
+
<View style={[fill, { backgroundColor: colors.primary }]}>{glyph}</View>
|
|
64
|
+
) : (
|
|
65
|
+
<ChromeGlass interactive style={fill}>
|
|
66
|
+
{glyph}
|
|
67
|
+
</ChromeGlass>
|
|
68
|
+
)}
|
|
57
69
|
</Pressable>
|
|
58
70
|
);
|
|
59
71
|
}
|