@myazahq/kyc-sdk-react-native 2.2.0 → 2.4.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/package.json +1 -2
- package/src/components/BrandBar.tsx +137 -0
- package/src/components/DialCodePicker.tsx +11 -17
- package/src/components/DocumentReviewSide.tsx +34 -14
- package/src/components/Icon.tsx +5 -0
- package/src/components/KycSheet.tsx +161 -180
- package/src/components/MediaSourceSheet.tsx +7 -37
- package/src/components/MyazaDateField.tsx +6 -20
- package/src/components/MyazaInput.tsx +6 -1
- package/src/components/MyazaSelect.tsx +20 -39
- package/src/components/PoweredBy.tsx +20 -15
- package/src/components/ProgressBar.tsx +91 -0
- package/src/components/SandboxBanner.tsx +92 -0
- package/src/components/StepHeader.tsx +15 -2
- package/src/components/StepIndicator.tsx +145 -31
- package/src/components/fonts.ts +23 -0
- package/src/components/glass/ChromeGlass.tsx +65 -0
- package/src/components/glass/FloatingSheet.tsx +190 -0
- package/src/components/glass/GlassSheet.tsx +38 -0
- package/src/components/glass/GlassSurface.tsx +31 -3
- 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/questionnaire.ts +45 -4
- package/src/config/workflowMerge.ts +1 -0
- package/src/emrtd/crypto.ts +1 -1
- package/src/emrtd/dg1.ts +55 -0
- package/src/emrtd/ec-curves.ts +142 -0
- package/src/emrtd/ec.ts +196 -0
- package/src/emrtd/index.ts +1 -0
- package/src/emrtd/mrzKey.ts +21 -1
- package/src/emrtd/open.ts +169 -0
- package/src/emrtd/pace-params.ts +169 -0
- package/src/emrtd/pace.ts +295 -0
- package/src/emrtd/secureMessaging.ts +32 -14
- package/src/emrtd/session.ts +124 -20
- package/src/emrtd/suites.ts +99 -0
- package/src/index.ts +1 -0
- package/src/lib/step-log.ts +43 -0
- package/src/lib/step-window.ts +96 -0
- package/src/liveness/useLiveness.ts +1 -1
- package/src/screens/IdTypeStep.tsx +15 -2
- package/src/screens/NfcStep.tsx +12 -0
- package/src/screens/QuestionnaireField.tsx +30 -0
- package/src/screens/QuestionnaireStep.tsx +3 -1
- package/src/screens/nfc/NfcSuccessPanel.tsx +13 -6
- package/src/services/deviceMetadata.ts +9 -1
- package/src/store/derive.ts +7 -0
- package/src/store/kycStore.ts +25 -1
- package/src/types/config.ts +22 -0
- package/src/types/workflow.ts +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myazahq/kyc-sdk-react-native",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Myaza KYC SDK for React Native (Expo) — ID verification, liveness detection, and document capture",
|
|
5
5
|
"author": "Flitstack Technologies Inc.",
|
|
6
6
|
"repository": {
|
|
@@ -61,7 +61,6 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@expo-google-fonts/karla": "^0.4.2",
|
|
63
63
|
"@expo-google-fonts/space-grotesk": "^0.4.1",
|
|
64
|
-
"@expo/ui": "~56.0.16",
|
|
65
64
|
"@expo/vector-icons": "^14.0.4",
|
|
66
65
|
"country-flag-icons": "^1.6.17",
|
|
67
66
|
"expo-application": "~56.0.3",
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Image, View } from 'react-native';
|
|
3
|
+
import { SvgXml } from 'react-native-svg';
|
|
4
|
+
|
|
5
|
+
import { radius, spacing } from '../config/theme';
|
|
6
|
+
import { useTheme } from './runtime';
|
|
7
|
+
import { MyazaText } from './Typography';
|
|
8
|
+
|
|
9
|
+
/** Whether a logo URI points at an SVG (extension check, query-safe). */
|
|
10
|
+
export function isSvgUri(uri: string): boolean {
|
|
11
|
+
const path = uri.split('?')[0] ?? uri;
|
|
12
|
+
return path.toLowerCase().endsWith('.svg');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Deliberately NOT on a Liquid Glass capsule, though it would balance the
|
|
16
|
+
// theme/close capsule opposite it. Glass at capsule scale is the material for
|
|
17
|
+
// controls — something that floats and responds to touch. The brand is a label,
|
|
18
|
+
// so a pill around it promises a tap that never does anything. The header block
|
|
19
|
+
// behind it is glass, which is the correct use: a surface, not a control.
|
|
20
|
+
//
|
|
21
|
+
// Persistent header brand — circular logo avatar + company name. A broken/missing
|
|
22
|
+
// logo image collapses the whole brand bar (mirrors the web SDK's `onError` and
|
|
23
|
+
// Flutter's `errorBuilder`), so the header never shows a broken-image box.
|
|
24
|
+
export function BrandBar({
|
|
25
|
+
logoUri,
|
|
26
|
+
companyName,
|
|
27
|
+
}: {
|
|
28
|
+
logoUri: string;
|
|
29
|
+
companyName: string;
|
|
30
|
+
}): React.ReactElement | null {
|
|
31
|
+
const { colors } = useTheme();
|
|
32
|
+
const [broken, setBroken] = useState(false);
|
|
33
|
+
const svg = isSvgUri(logoUri);
|
|
34
|
+
// SVG logos render through SvgXml with SELF-FETCHED markup — the exact
|
|
35
|
+
// pipeline the country flags use, which is proven on-device. SvgUri's own
|
|
36
|
+
// fetch/sizing was flaky here (the favicon rendered at intrinsic size
|
|
37
|
+
// instead of filling the chip).
|
|
38
|
+
const [xml, setXml] = useState<string | null>(null);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!svg) return;
|
|
41
|
+
let cancelled = false;
|
|
42
|
+
fetch(logoUri)
|
|
43
|
+
.then((r) => (r.ok ? r.text() : Promise.reject(new Error(String(r.status)))))
|
|
44
|
+
.then((text) => {
|
|
45
|
+
if (!cancelled) setXml(text);
|
|
46
|
+
})
|
|
47
|
+
.catch(() => {
|
|
48
|
+
if (!cancelled) setBroken(true);
|
|
49
|
+
});
|
|
50
|
+
return () => {
|
|
51
|
+
cancelled = true;
|
|
52
|
+
};
|
|
53
|
+
}, [logoUri, svg]);
|
|
54
|
+
if (broken) return null;
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
{/* TWO layers, deliberately.
|
|
58
|
+
|
|
59
|
+
A shadow and `overflow: 'hidden'` CANNOT live on the same view on iOS:
|
|
60
|
+
overflow-hidden compiles to `masksToBounds`, which clips the shadow
|
|
61
|
+
away with everything else outside the bounds. Android draws `elevation`
|
|
62
|
+
outside the view, so it survived there — which is why the chip lost its
|
|
63
|
+
ring on iPhone only.
|
|
64
|
+
|
|
65
|
+
So the outer view owns the border and shadow and does NOT clip, and the
|
|
66
|
+
inner one clips the logo. Same split Flutter gets for free by putting
|
|
67
|
+
the border on the Container's decoration and the crop in a ClipOval.
|
|
68
|
+
|
|
69
|
+
The white plate is load-bearing, not decoration: brand logos are
|
|
70
|
+
routinely dark artwork on a transparent background (the import picks up
|
|
71
|
+
favicons), which would vanish against a dark header. */}
|
|
72
|
+
<View
|
|
73
|
+
style={{
|
|
74
|
+
width: 28,
|
|
75
|
+
height: 28,
|
|
76
|
+
borderRadius: radius.full,
|
|
77
|
+
backgroundColor: '#FFFFFF',
|
|
78
|
+
// Light ring + soft shadow — the same treatment as the Flutter SDK's
|
|
79
|
+
// brand chip (black 5% ring, black 6% blur-4 y-1 shadow) and the web
|
|
80
|
+
// SDK's ring-1 ring-black/5.
|
|
81
|
+
borderWidth: 1,
|
|
82
|
+
borderColor: 'rgba(0,0,0,0.05)',
|
|
83
|
+
shadowColor: '#000000',
|
|
84
|
+
shadowOpacity: 0.06,
|
|
85
|
+
shadowRadius: 4,
|
|
86
|
+
shadowOffset: { width: 0, height: 1 },
|
|
87
|
+
elevation: 1,
|
|
88
|
+
marginRight: spacing.sm,
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<View
|
|
94
|
+
style={{
|
|
95
|
+
// Fills the parent's CONTENT box (inside its 1px border) and is the
|
|
96
|
+
// only thing that clips, so the logo reaches the ring on every side.
|
|
97
|
+
alignSelf: 'stretch',
|
|
98
|
+
flex: 1,
|
|
99
|
+
borderRadius: radius.full,
|
|
100
|
+
overflow: 'hidden',
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
{/* Fill + cover-crop, matching Flutter's SizedBox.expand + BoxFit.cover.
|
|
104
|
+
|
|
105
|
+
Sized in PERCENTAGES, not the literal 26 this used to hardcode: that
|
|
106
|
+
number was "28 minus the 1px border on each side", so it silently
|
|
107
|
+
went wrong the moment either value changed, and it left the logo a
|
|
108
|
+
pixel short of the ring. The clipping parent now owns the geometry.
|
|
109
|
+
|
|
110
|
+
SVG logos are common here because the brand import picks up site
|
|
111
|
+
favicons, and <Image> cannot decode SVG at all — hence the fetched
|
|
112
|
+
markup through SvgXml. `slice` is the SVG spelling of cover-crop. */}
|
|
113
|
+
{svg ? (
|
|
114
|
+
xml ? (
|
|
115
|
+
<SvgXml xml={xml} width="100%" height="100%" preserveAspectRatio="xMidYMid slice" />
|
|
116
|
+
) : null
|
|
117
|
+
) : (
|
|
118
|
+
<Image
|
|
119
|
+
source={{ uri: logoUri }}
|
|
120
|
+
style={{ width: '100%', height: '100%' }}
|
|
121
|
+
resizeMode="cover"
|
|
122
|
+
onError={() => setBroken(true)}
|
|
123
|
+
/>
|
|
124
|
+
)}
|
|
125
|
+
</View>
|
|
126
|
+
</View>
|
|
127
|
+
<MyazaText
|
|
128
|
+
variant="heading3"
|
|
129
|
+
numberOfLines={1}
|
|
130
|
+
color={colors.textDark}
|
|
131
|
+
style={{ flexShrink: 1, fontSize: 14 }}
|
|
132
|
+
>
|
|
133
|
+
{companyName}
|
|
134
|
+
</MyazaText>
|
|
135
|
+
</>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -2,19 +2,19 @@ import React, { useEffect, useMemo, useState } from 'react';
|
|
|
2
2
|
import {
|
|
3
3
|
FlatList,
|
|
4
4
|
Keyboard,
|
|
5
|
-
Modal,
|
|
6
5
|
Platform,
|
|
7
6
|
Pressable,
|
|
8
7
|
useWindowDimensions,
|
|
9
8
|
View,
|
|
10
9
|
} from 'react-native';
|
|
11
10
|
|
|
12
|
-
import {
|
|
11
|
+
import { spacing } from '../config/theme';
|
|
13
12
|
import { useTheme } from './runtime';
|
|
14
13
|
import { MyazaText } from './Typography';
|
|
15
14
|
import { MyazaInput } from './MyazaInput';
|
|
16
15
|
import { Icon } from './Icon';
|
|
17
16
|
import { CountryFlag } from './CountryFlag';
|
|
17
|
+
import { FloatingSheet } from './glass/FloatingSheet';
|
|
18
18
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
// THE country sheet — the phone field's dial-code picker, generalised.
|
|
@@ -98,19 +98,14 @@ export function DialCodePicker({
|
|
|
98
98
|
};
|
|
99
99
|
|
|
100
100
|
return (
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
borderTopRightRadius: radius.lg,
|
|
110
|
-
paddingTop: spacing.md,
|
|
111
|
-
paddingBottom: spacing.sm,
|
|
112
|
-
}}
|
|
113
|
-
>
|
|
101
|
+
<FloatingSheet
|
|
102
|
+
visible={visible}
|
|
103
|
+
onClose={close}
|
|
104
|
+
maxHeight={maxHeight}
|
|
105
|
+
// Ride above a raised keyboard — this sheet's whole job is a search box.
|
|
106
|
+
bottomOffset={keyboard}
|
|
107
|
+
closeLabel="Close country picker"
|
|
108
|
+
>
|
|
114
109
|
<View style={{ paddingHorizontal: spacing.md, paddingBottom: spacing.sm }}>
|
|
115
110
|
<MyazaInput
|
|
116
111
|
value={query}
|
|
@@ -141,8 +136,7 @@ export function DialCodePicker({
|
|
|
141
136
|
/>
|
|
142
137
|
)}
|
|
143
138
|
/>
|
|
144
|
-
|
|
145
|
-
</Modal>
|
|
139
|
+
</FloatingSheet>
|
|
146
140
|
);
|
|
147
141
|
}
|
|
148
142
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Image, Pressable, View } from 'react-native';
|
|
2
|
+
import { Image, Pressable, View, type ViewStyle } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { useTheme } from './runtime';
|
|
5
5
|
import { Icon } from './Icon';
|
|
6
6
|
import { MyazaText } from './Typography';
|
|
7
7
|
import { radius, spacing } from '../config/theme';
|
|
8
|
+
import { CHROME_SCRIM, ChromeGlass } from './glass/ChromeGlass';
|
|
9
|
+
|
|
10
|
+
/** The chip's flat backing, and still the fallback off iOS 26. */
|
|
11
|
+
const CHIP_SCRIM = CHROME_SCRIM;
|
|
8
12
|
|
|
9
13
|
// ─── One captured side, and its enlarged view ─────────────────────────────────
|
|
10
14
|
//
|
|
@@ -67,7 +71,7 @@ export function DocumentReviewThumb({
|
|
|
67
71
|
{/* Enlarging is a tap on the image, which nothing announces — so the
|
|
68
72
|
image says so itself. */}
|
|
69
73
|
<View style={{ position: 'absolute', right: spacing.md, bottom: spacing.md }} pointerEvents="none">
|
|
70
|
-
<Chip icon>
|
|
74
|
+
<Chip icon glass>
|
|
71
75
|
<Icon name="maximize" size={18} color="#FFFFFF" />
|
|
72
76
|
</Chip>
|
|
73
77
|
</View>
|
|
@@ -116,20 +120,36 @@ export function Chip({
|
|
|
116
120
|
children,
|
|
117
121
|
/** Icon chips are square and need room around the glyph, not text padding. */
|
|
118
122
|
icon = false,
|
|
123
|
+
/**
|
|
124
|
+
* Render on Liquid Glass instead of the flat scrim.
|
|
125
|
+
*
|
|
126
|
+
* Only for a chip that ANNOUNCES AN ACTION. It looks like an exception to the
|
|
127
|
+
* "glass is for controls" rule, since the chip itself is `pointerEvents:
|
|
128
|
+
* none` — but the photo behind it is the button, and the chip sits inside
|
|
129
|
+
* that target, so a tap on it really does fire. The promise is kept.
|
|
130
|
+
*
|
|
131
|
+
* A chip that only labels something (FRONT / BACK) stays flat: glass would
|
|
132
|
+
* offer a tap it cannot honour.
|
|
133
|
+
*/
|
|
134
|
+
glass = false,
|
|
119
135
|
}: {
|
|
120
136
|
children: React.ReactNode;
|
|
121
137
|
icon?: boolean;
|
|
138
|
+
glass?: boolean;
|
|
122
139
|
}): React.ReactElement {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
{
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
const shape: ViewStyle = {
|
|
141
|
+
paddingHorizontal: icon ? 8 : 10,
|
|
142
|
+
paddingVertical: icon ? 8 : 5,
|
|
143
|
+
borderRadius: radius.full,
|
|
144
|
+
};
|
|
145
|
+
// Not `interactive`: the chip never receives the touch (the image below it
|
|
146
|
+
// does), so asking the glass to react to one would be asking for nothing.
|
|
147
|
+
if (glass) {
|
|
148
|
+
return (
|
|
149
|
+
<ChromeGlass scrim={CHIP_SCRIM} style={shape}>
|
|
150
|
+
{children}
|
|
151
|
+
</ChromeGlass>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return <View style={[shape, { backgroundColor: CHIP_SCRIM }]}>{children}</View>;
|
|
135
155
|
}
|
package/src/components/Icon.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
CreditCard,
|
|
18
18
|
Fingerprint,
|
|
19
19
|
FileText,
|
|
20
|
+
FlaskConical,
|
|
20
21
|
IdCard,
|
|
21
22
|
Image as ImageIcon,
|
|
22
23
|
Landmark,
|
|
@@ -68,6 +69,7 @@ export type IconName =
|
|
|
68
69
|
| 'check'
|
|
69
70
|
| 'lock'
|
|
70
71
|
| 'alert'
|
|
72
|
+
| 'flask'
|
|
71
73
|
| 'refresh'
|
|
72
74
|
| 'maximize'
|
|
73
75
|
| 'calendar'
|
|
@@ -128,6 +130,9 @@ const ICONS: Record<IconName, LucideIcon> = {
|
|
|
128
130
|
check: Check,
|
|
129
131
|
lock: Lock,
|
|
130
132
|
alert: CircleAlert,
|
|
133
|
+
// The environment banner's mark, shared with the web SDK (same lucide glyph)
|
|
134
|
+
// and mirrored by Flutter's Icons.science_outlined.
|
|
135
|
+
flask: FlaskConical,
|
|
131
136
|
refresh: RefreshCw,
|
|
132
137
|
maximize: Maximize2,
|
|
133
138
|
calendar: Calendar,
|