@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.
Files changed (51) hide show
  1. package/package.json +1 -2
  2. package/src/components/BrandBar.tsx +137 -0
  3. package/src/components/DialCodePicker.tsx +11 -17
  4. package/src/components/DocumentReviewSide.tsx +34 -14
  5. package/src/components/Icon.tsx +5 -0
  6. package/src/components/KycSheet.tsx +161 -180
  7. package/src/components/MediaSourceSheet.tsx +7 -37
  8. package/src/components/MyazaDateField.tsx +6 -20
  9. package/src/components/MyazaInput.tsx +6 -1
  10. package/src/components/MyazaSelect.tsx +20 -39
  11. package/src/components/PoweredBy.tsx +20 -15
  12. package/src/components/ProgressBar.tsx +91 -0
  13. package/src/components/SandboxBanner.tsx +92 -0
  14. package/src/components/StepHeader.tsx +15 -2
  15. package/src/components/StepIndicator.tsx +145 -31
  16. package/src/components/fonts.ts +23 -0
  17. package/src/components/glass/ChromeGlass.tsx +65 -0
  18. package/src/components/glass/FloatingSheet.tsx +190 -0
  19. package/src/components/glass/GlassSheet.tsx +38 -0
  20. package/src/components/glass/GlassSurface.tsx +31 -3
  21. package/src/components/viewfinder/ImmersiveBottomBar.tsx +28 -17
  22. package/src/components/viewfinder/ImmersiveControls.tsx +26 -14
  23. package/src/components/viewfinder/ViewfinderControls.tsx +29 -7
  24. package/src/config/questionnaire.ts +45 -4
  25. package/src/config/workflowMerge.ts +1 -0
  26. package/src/emrtd/crypto.ts +1 -1
  27. package/src/emrtd/dg1.ts +55 -0
  28. package/src/emrtd/ec-curves.ts +142 -0
  29. package/src/emrtd/ec.ts +196 -0
  30. package/src/emrtd/index.ts +1 -0
  31. package/src/emrtd/mrzKey.ts +21 -1
  32. package/src/emrtd/open.ts +169 -0
  33. package/src/emrtd/pace-params.ts +169 -0
  34. package/src/emrtd/pace.ts +295 -0
  35. package/src/emrtd/secureMessaging.ts +32 -14
  36. package/src/emrtd/session.ts +124 -20
  37. package/src/emrtd/suites.ts +99 -0
  38. package/src/index.ts +1 -0
  39. package/src/lib/step-log.ts +43 -0
  40. package/src/lib/step-window.ts +96 -0
  41. package/src/liveness/useLiveness.ts +1 -1
  42. package/src/screens/IdTypeStep.tsx +15 -2
  43. package/src/screens/NfcStep.tsx +12 -0
  44. package/src/screens/QuestionnaireField.tsx +30 -0
  45. package/src/screens/QuestionnaireStep.tsx +3 -1
  46. package/src/screens/nfc/NfcSuccessPanel.tsx +13 -6
  47. package/src/services/deviceMetadata.ts +9 -1
  48. package/src/store/derive.ts +7 -0
  49. package/src/store/kycStore.ts +25 -1
  50. package/src/types/config.ts +22 -0
  51. 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.2.0",
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 { radius, spacing } from '../config/theme';
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
- <Modal visible={visible} animationType="slide" transparent onRequestClose={close}>
102
- <Pressable style={{ flex: 1, backgroundColor: '#00000066' }} onPress={close} />
103
- <View
104
- style={{
105
- maxHeight,
106
- marginBottom: keyboard,
107
- backgroundColor: colors.background,
108
- borderTopLeftRadius: radius.lg,
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
- </View>
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
- return (
124
- <View
125
- style={{
126
- paddingHorizontal: icon ? 8 : 10,
127
- paddingVertical: icon ? 8 : 5,
128
- borderRadius: radius.full,
129
- backgroundColor: 'rgba(0,0,0,0.55)',
130
- }}
131
- >
132
- {children}
133
- </View>
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
  }
@@ -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,