@myazahq/kyc-sdk-react-native 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/KycSdkReactNative.podspec +32 -0
  2. package/LICENSE +21 -0
  3. package/README.md +164 -0
  4. package/android/CMakeLists.txt +29 -0
  5. package/android/build.gradle +110 -0
  6. package/android/src/main/AndroidManifest.xml +12 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +12 -0
  8. package/android/src/main/java/co/myazahq/kyc/rn/MyazaFaceDetectorPackage.kt +36 -0
  9. package/android/src/main/java/co/myazahq/kyc/rn/MyazaStatusBarModule.kt +64 -0
  10. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +134 -0
  11. package/app.plugin.js +55 -0
  12. package/expo-module.config.json +6 -0
  13. package/ios/HybridMyazaFaceDetector.swift +233 -0
  14. package/package.json +84 -0
  15. package/react-native.config.js +23 -0
  16. package/src/MyazaKYC.tsx +235 -0
  17. package/src/__tests__/cardCrop.test.ts +39 -0
  18. package/src/__tests__/deviceMetadata.test.ts +34 -0
  19. package/src/__tests__/errors.test.ts +34 -0
  20. package/src/__tests__/flow.test.ts +61 -0
  21. package/src/__tests__/gestureDetector.test.ts +37 -0
  22. package/src/__tests__/liveness.test.ts +112 -0
  23. package/src/__tests__/resolveUrl.test.ts +64 -0
  24. package/src/__tests__/validators.test.ts +38 -0
  25. package/src/assets/liveness/Blink.gif +0 -0
  26. package/src/assets/liveness/Nod.gif +0 -0
  27. package/src/assets/liveness/Smile.gif +0 -0
  28. package/src/assets/liveness/Turn.gif +0 -0
  29. package/src/components/CameraPermissionView.tsx +116 -0
  30. package/src/components/CameraViewfinder.tsx +156 -0
  31. package/src/components/CountryFlag.tsx +52 -0
  32. package/src/components/DocumentCropper.tsx +325 -0
  33. package/src/components/GlassIconButton.tsx +92 -0
  34. package/src/components/Icon.tsx +125 -0
  35. package/src/components/KycFlow.tsx +205 -0
  36. package/src/components/KycSheet.tsx +224 -0
  37. package/src/components/MyazaAlert.tsx +57 -0
  38. package/src/components/MyazaButton.tsx +101 -0
  39. package/src/components/MyazaCard.tsx +48 -0
  40. package/src/components/MyazaInput.tsx +112 -0
  41. package/src/components/MyazaPulseLoader.tsx +71 -0
  42. package/src/components/StatusBarController.tsx +42 -0
  43. package/src/components/StepHeader.tsx +56 -0
  44. package/src/components/StepIndicator.tsx +74 -0
  45. package/src/components/Typography.tsx +69 -0
  46. package/src/components/fonts.ts +49 -0
  47. package/src/components/glass/GlassGroup.tsx +34 -0
  48. package/src/components/glass/GlassSurface.tsx +64 -0
  49. package/src/components/runtime.tsx +93 -0
  50. package/src/components/toast.tsx +154 -0
  51. package/src/components/useBranding.ts +27 -0
  52. package/src/components/useVideoRecorder.ts +122 -0
  53. package/src/config/captureSettings.ts +67 -0
  54. package/src/config/idTypes.ts +79 -0
  55. package/src/config/theme.ts +186 -0
  56. package/src/index.ts +54 -0
  57. package/src/liveness/challengeManager.ts +130 -0
  58. package/src/liveness/faceDetector.ts +79 -0
  59. package/src/liveness/gestureDetector.ts +80 -0
  60. package/src/liveness/speech.ts +66 -0
  61. package/src/liveness/types.ts +99 -0
  62. package/src/liveness/useLiveness.ts +484 -0
  63. package/src/liveness/visionCameraFaceDetector.ts +118 -0
  64. package/src/screens/ConsentStep.tsx +164 -0
  65. package/src/screens/DocumentCaptureStep.tsx +500 -0
  66. package/src/screens/IdInputStep.tsx +79 -0
  67. package/src/screens/IdTypeStep.tsx +142 -0
  68. package/src/screens/LivenessAvatar.tsx +69 -0
  69. package/src/screens/LivenessStep.tsx +615 -0
  70. package/src/screens/SubmittedStep.tsx +177 -0
  71. package/src/services/api.ts +291 -0
  72. package/src/services/cardCrop.ts +52 -0
  73. package/src/services/deviceMetadata.ts +185 -0
  74. package/src/services/errors.ts +92 -0
  75. package/src/services/mediaCompress.ts +129 -0
  76. package/src/services/resolveUrl.ts +98 -0
  77. package/src/services/retry.ts +70 -0
  78. package/src/services/validators.ts +103 -0
  79. package/src/specs/MyazaFaceDetector.nitro.ts +44 -0
  80. package/src/store/kycStore.ts +288 -0
  81. package/src/store/serverConfig.ts +78 -0
  82. package/src/types/config.ts +239 -0
  83. package/src/types/country-flag-icons.d.ts +6 -0
  84. package/src/types/verification.ts +79 -0
  85. package/src/utils/platform.ts +23 -0
  86. package/src/utils/tokens.ts +10 -0
  87. package/src/utils/uuid.ts +31 -0
@@ -0,0 +1,112 @@
1
+ import React, { useState } from 'react';
2
+ import {
3
+ TextInput,
4
+ View,
5
+ type KeyboardTypeOptions,
6
+ type TextInputProps,
7
+ } from 'react-native';
8
+
9
+ type FocusHandler = NonNullable<TextInputProps['onFocus']>;
10
+ type BlurHandler = NonNullable<TextInputProps['onBlur']>;
11
+
12
+ import { radius, sizing, spacing } from '../config/theme';
13
+ import { useTheme } from './runtime';
14
+ import { MyazaText } from './Typography';
15
+
16
+ // Branded text input with label, helper, and error states. Mirrors the Flutter
17
+ // SDK's MyazaInput.
18
+
19
+ export interface MyazaInputProps {
20
+ value: string;
21
+ onChangeText: (text: string) => void;
22
+ label?: string;
23
+ placeholder?: string;
24
+ error?: string | null;
25
+ helper?: string;
26
+ keyboardType?: KeyboardTypeOptions;
27
+ autoCapitalize?: 'none' | 'characters';
28
+ maxLength?: number;
29
+ editable?: boolean;
30
+ /** Focus the field on mount (matches the Flutter SDK's id-input autofocus). */
31
+ autoFocus?: boolean;
32
+ onFocus?: FocusHandler;
33
+ onBlur?: BlurHandler;
34
+ }
35
+
36
+ export function MyazaInput({
37
+ value,
38
+ onChangeText,
39
+ label,
40
+ placeholder,
41
+ error,
42
+ helper,
43
+ keyboardType = 'default',
44
+ autoCapitalize = 'none',
45
+ maxLength,
46
+ editable = true,
47
+ autoFocus = false,
48
+ onFocus,
49
+ onBlur,
50
+ }: MyazaInputProps): React.ReactElement {
51
+ const { colors } = useTheme();
52
+ const [focused, setFocused] = useState(false);
53
+
54
+ // Mirror the Flutter SDK's MyazaInput border states:
55
+ // focused + error → error / 2px · focused → primary / 2px
56
+ // error → error / 1.5px · default → border / 1px
57
+ const borderColor = error ? colors.error : focused ? colors.primary : colors.border;
58
+ const borderWidth = focused ? 2 : error ? 1.5 : 1;
59
+
60
+ const handleFocus: FocusHandler = (e) => {
61
+ setFocused(true);
62
+ onFocus?.(e);
63
+ };
64
+ const handleBlur: BlurHandler = (e) => {
65
+ setFocused(false);
66
+ onBlur?.(e);
67
+ };
68
+
69
+ return (
70
+ <View>
71
+ {label ? (
72
+ <MyazaText variant="label" style={{ marginBottom: spacing.sm }}>
73
+ {label}
74
+ </MyazaText>
75
+ ) : null}
76
+ <TextInput
77
+ value={value}
78
+ onChangeText={onChangeText}
79
+ placeholder={placeholder}
80
+ placeholderTextColor={colors.textMuted}
81
+ keyboardType={keyboardType}
82
+ autoCapitalize={autoCapitalize}
83
+ maxLength={maxLength}
84
+ editable={editable}
85
+ autoFocus={autoFocus}
86
+ onFocus={handleFocus}
87
+ onBlur={handleBlur}
88
+ style={{
89
+ height: sizing.inputHeight,
90
+ borderWidth,
91
+ borderColor,
92
+ borderRadius: radius.sm,
93
+ // Keep text from shifting when the border thickens on focus: pad so
94
+ // (border + padding) stays constant across states (max border = 2).
95
+ paddingHorizontal: spacing.md + (2 - borderWidth),
96
+ color: colors.textDark,
97
+ backgroundColor: colors.background,
98
+ fontSize: 16,
99
+ }}
100
+ />
101
+ {error ? (
102
+ <MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.xs }}>
103
+ {error}
104
+ </MyazaText>
105
+ ) : helper ? (
106
+ <MyazaText variant="bodySmall" style={{ marginTop: spacing.xs }}>
107
+ {helper}
108
+ </MyazaText>
109
+ ) : null}
110
+ </View>
111
+ );
112
+ }
@@ -0,0 +1,71 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { ActivityIndicator, Animated, Easing, View } from 'react-native';
3
+
4
+ import { useTheme } from './runtime';
5
+
6
+ // Branded loading indicator — the RN mirror of the Flutter SDK's
7
+ // `MyazaPulseLoader` (lib/src/widgets/myaza_pulse_loader.dart): a pulsing outer
8
+ // ring (border, scale 0.85→1.05 + fade-out, 1s loop) around an inner tinted
9
+ // circle holding a spinner. Shared by the ID-type loading state and the
10
+ // submitting screen so the loader looks consistent across the flow and matches
11
+ // the web/Flutter SDKs.
12
+
13
+ export interface MyazaPulseLoaderProps {
14
+ /** Outer ring diameter. The inner spinner badge scales to 70% of this. */
15
+ size?: number;
16
+ }
17
+
18
+ export function MyazaPulseLoader({ size = 80 }: MyazaPulseLoaderProps): React.ReactElement {
19
+ const { colors } = useTheme();
20
+ const pulse = useRef(new Animated.Value(0)).current;
21
+
22
+ useEffect(() => {
23
+ const loop = Animated.loop(
24
+ Animated.timing(pulse, {
25
+ toValue: 1,
26
+ duration: 1000,
27
+ easing: Easing.inOut(Easing.ease),
28
+ useNativeDriver: true,
29
+ }),
30
+ );
31
+ loop.start();
32
+ return () => loop.stop();
33
+ }, [pulse]);
34
+
35
+ // Outer ring: scales 0.85 → 1.05 while fading 0.8 → 0 (mirrors Flutter's
36
+ // .scale(...).fadeOut(begin: 0.8)).
37
+ const scale = pulse.interpolate({ inputRange: [0, 1], outputRange: [0.85, 1.05] });
38
+ const opacity = pulse.interpolate({ inputRange: [0, 1], outputRange: [0.8, 0] });
39
+ const inner = size * 0.7;
40
+
41
+ return (
42
+ <View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
43
+ {/* Pulsing outer ring (border only). */}
44
+ <Animated.View
45
+ style={{
46
+ position: 'absolute',
47
+ width: size,
48
+ height: size,
49
+ borderRadius: size / 2,
50
+ borderWidth: 2,
51
+ borderColor: colors.primary,
52
+ opacity,
53
+ transform: [{ scale }],
54
+ }}
55
+ />
56
+ {/* Inner tinted circle with a spinner. */}
57
+ <View
58
+ style={{
59
+ width: inner,
60
+ height: inner,
61
+ borderRadius: inner / 2,
62
+ backgroundColor: `${colors.primary}1A`, // ~10% primary
63
+ alignItems: 'center',
64
+ justifyContent: 'center',
65
+ }}
66
+ >
67
+ <ActivityIndicator size="small" color={colors.primary} />
68
+ </View>
69
+ </View>
70
+ );
71
+ }
@@ -0,0 +1,42 @@
1
+ import React, { useCallback, useEffect, useRef } from 'react';
2
+ import { findNodeHandle, NativeModules, Platform, StyleSheet, View } from 'react-native';
3
+
4
+ // Themes the Android modal's status/navigation bars from the SDK theme. RN's
5
+ // <Modal> runs in its own Dialog window that JS StatusBar/SystemBars APIs can't
6
+ // reach (they target the activity window). So we render a tiny real native view
7
+ // INSIDE the modal and hand its tag to MyazaStatusBarModule, which resolves the
8
+ // view and applies the bar appearance to ITS window (the Dialog). Native modules
9
+ // (unlike legacy view managers) work fine on the New Architecture. Android-only.
10
+
11
+ const StatusBarModule: { setBarStyle?: (viewTag: number, light: boolean) => void } | undefined =
12
+ Platform.OS === 'android' ? NativeModules.MyazaStatusBarModule : undefined;
13
+
14
+ /** `barStyle: 'light'` = light glyphs (dark bg), `'dark'` = dark glyphs (light bg). */
15
+ export function StatusBarController({ barStyle }: { barStyle: 'light' | 'dark' }): React.ReactElement | null {
16
+ const ref = useRef<View>(null);
17
+
18
+ const apply = useCallback(() => {
19
+ if (!StatusBarModule?.setBarStyle) return;
20
+ const tag = findNodeHandle(ref.current);
21
+ if (tag != null) StatusBarModule.setBarStyle(tag, barStyle === 'light');
22
+ }, [barStyle]);
23
+
24
+ // Re-apply whenever the theme flips. On the FIRST open the modal's Dialog window
25
+ // resets its own bar appearance as it finishes presenting, which overrides an
26
+ // immediate apply (the bar would stay on the host style until a manual toggle).
27
+ // So re-apply on a couple of short delays past the present animation. `onLayout`
28
+ // additionally covers the apply once the view attaches to the Dialog.
29
+ useEffect(() => {
30
+ apply();
31
+ const timers = [setTimeout(apply, 250), setTimeout(apply, 600)];
32
+ return () => timers.forEach(clearTimeout);
33
+ }, [apply]);
34
+
35
+ if (Platform.OS !== 'android') return null;
36
+ // `collapsable={false}` keeps it a real native view so the module can resolve it.
37
+ return <View ref={ref} collapsable={false} onLayout={apply} style={styles.hidden} pointerEvents="none" />;
38
+ }
39
+
40
+ const styles = StyleSheet.create({
41
+ hidden: { position: 'absolute', width: 0, height: 0 },
42
+ });
@@ -0,0 +1,56 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../config/theme';
5
+ import type { SupportedCountry } from '../types/config';
6
+ import { useTheme } from './runtime';
7
+ import { MyazaText } from './Typography';
8
+ import { GlassIconButton } from './GlassIconButton';
9
+ import { CountryFlag } from './CountryFlag';
10
+
11
+ // Step title row — 1:1 with the Flutter SDK's StepHeader: optional back button,
12
+ // title (heading2) + optional country flag, then an optional description below
13
+ // (indented to align under the title when a back button is present).
14
+
15
+ const BACK_FOOTPRINT = 28 + spacing.sm; // back button width + gap
16
+
17
+ export interface StepHeaderProps {
18
+ title: string;
19
+ description?: string | null;
20
+ onBack?: (() => void) | null;
21
+ country?: SupportedCountry | null;
22
+ }
23
+
24
+ export function StepHeader({ title, description, onBack, country }: StepHeaderProps): React.ReactElement {
25
+ const { colors } = useTheme();
26
+ return (
27
+ <View>
28
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>
29
+ {onBack ? (
30
+ <View style={{ marginRight: spacing.sm }}>
31
+ <GlassIconButton icon="back" onPress={onBack} size={28} iconSize={22} accessibilityLabel="Back" />
32
+ </View>
33
+ ) : null}
34
+ <View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
35
+ <MyazaText variant="heading2" numberOfLines={2} style={{ flexShrink: 1 }}>
36
+ {title}
37
+ </MyazaText>
38
+ {country ? (
39
+ <View style={{ marginLeft: spacing.sm }}>
40
+ <CountryFlag country={country} size={20} />
41
+ </View>
42
+ ) : null}
43
+ </View>
44
+ </View>
45
+ {description ? (
46
+ <MyazaText
47
+ variant="bodyMedium"
48
+ color={colors.textSecondary}
49
+ style={{ marginTop: spacing.xs, marginLeft: onBack ? BACK_FOOTPRINT : 0 }}
50
+ >
51
+ {description}
52
+ </MyazaText>
53
+ ) : null}
54
+ </View>
55
+ );
56
+ }
@@ -0,0 +1,74 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../config/theme';
5
+ import { useTheme } from './runtime';
6
+ import { MyazaText } from './Typography';
7
+ import { Icon } from './Icon';
8
+
9
+ // Segmented numbered step indicator — 1:1 with the Flutter SDK's _StepIndicator.
10
+ // N circles connected by thin lines:
11
+ // • completed → filled primary + white check
12
+ // • active → filled primary + white number
13
+ // • upcoming → outlined (primary200) + muted number
14
+ // activeIndex = round(progress * stepCount) - 1.
15
+
16
+ export interface StepIndicatorProps {
17
+ /** 0.0–1.0 progress fraction. */
18
+ progress: number;
19
+ stepCount: number;
20
+ }
21
+
22
+ export function StepIndicator({ progress, stepCount }: StepIndicatorProps): React.ReactElement {
23
+ const { colors } = useTheme();
24
+ const active = Math.round(progress * stepCount) - 1;
25
+
26
+ return (
27
+ <View style={{ flexDirection: 'row', alignItems: 'center', paddingHorizontal: spacing.md }}>
28
+ {Array.from({ length: stepCount }).map((_, i) => {
29
+ const completed = i < active;
30
+ const isActive = i === active;
31
+ const filled = completed || isActive;
32
+ return (
33
+ <React.Fragment key={i}>
34
+ <View
35
+ style={{
36
+ width: 26,
37
+ height: 26,
38
+ borderRadius: 13,
39
+ alignItems: 'center',
40
+ justifyContent: 'center',
41
+ backgroundColor: filled ? colors.primary : 'transparent',
42
+ borderWidth: 1.5,
43
+ borderColor: filled ? colors.primary : colors.primary200,
44
+ }}
45
+ >
46
+ {completed ? (
47
+ <Icon name="check" size={14} color="#FFFFFF" />
48
+ ) : (
49
+ <MyazaText
50
+ variant="bodySmall"
51
+ color={isActive ? '#FFFFFF' : colors.textMuted}
52
+ style={{ fontSize: 11, fontWeight: '700' }}
53
+ >
54
+ {i + 1}
55
+ </MyazaText>
56
+ )}
57
+ </View>
58
+ {i < stepCount - 1 ? (
59
+ <View
60
+ style={{
61
+ flex: 1,
62
+ height: 2,
63
+ marginHorizontal: 3,
64
+ borderRadius: 1,
65
+ backgroundColor: completed ? colors.primary : colors.primary200,
66
+ }}
67
+ />
68
+ ) : null}
69
+ </React.Fragment>
70
+ );
71
+ })}
72
+ </View>
73
+ );
74
+ }
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import { StyleSheet, Text, type TextProps, type TextStyle } from 'react-native';
3
+
4
+ import { useTheme } from './runtime';
5
+ import { fontFamilyFor } from './fonts';
6
+
7
+ // Branded text. `variant` picks size/weight; headings render in Space Grotesk and
8
+ // body/labels in Karla — the SAME families the Flutter SDK uses (google_fonts) —
9
+ // resolved per weight by `fontFamilyFor` once expo-font has loaded them.
10
+
11
+ export type TextVariant =
12
+ | 'heading1'
13
+ | 'heading2'
14
+ | 'heading3'
15
+ | 'body'
16
+ | 'bodyMedium'
17
+ | 'bodySmall'
18
+ | 'label'
19
+ | 'button';
20
+
21
+ const VARIANTS: Record<TextVariant, TextStyle> = {
22
+ heading1: { fontSize: 24, fontWeight: '700' },
23
+ heading2: { fontSize: 20, fontWeight: '600' },
24
+ heading3: { fontSize: 16, fontWeight: '600' },
25
+ body: { fontSize: 16, fontWeight: '400' },
26
+ bodyMedium: { fontSize: 14, fontWeight: '400' },
27
+ bodySmall: { fontSize: 12, fontWeight: '400' },
28
+ label: { fontSize: 14, fontWeight: '500' },
29
+ button: { fontSize: 16, fontWeight: '600' },
30
+ };
31
+
32
+ const HEADINGS = new Set<TextVariant>(['heading1', 'heading2', 'heading3']);
33
+
34
+ export interface MyazaTextProps extends TextProps {
35
+ variant?: TextVariant;
36
+ color?: string;
37
+ }
38
+
39
+ export function MyazaText({
40
+ variant = 'body',
41
+ color,
42
+ style,
43
+ ...rest
44
+ }: MyazaTextProps): React.ReactElement {
45
+ const { colors, fontsLoaded } = useTheme();
46
+ const defaultColor =
47
+ variant === 'bodyMedium'
48
+ ? colors.textSecondary
49
+ : variant === 'bodySmall'
50
+ ? colors.textMuted
51
+ : colors.textDark;
52
+
53
+ // Flatten variant + caller style so an inline `fontWeight` override selects the
54
+ // right Space Grotesk / Karla weight (custom fonts ignore `fontWeight`, so the
55
+ // weight must be baked into the family).
56
+ const flat = (StyleSheet.flatten([VARIANTS[variant], style]) ?? {}) as TextStyle;
57
+ const family = fontFamilyFor(HEADINGS.has(variant), flat.fontWeight, fontsLoaded);
58
+
59
+ return (
60
+ <Text
61
+ style={[
62
+ flat,
63
+ { color: color ?? defaultColor },
64
+ family ? { fontFamily: family, fontWeight: undefined } : null,
65
+ ]}
66
+ {...rest}
67
+ />
68
+ );
69
+ }
@@ -0,0 +1,49 @@
1
+ import { useFonts } from 'expo-font';
2
+ import { SpaceGrotesk_500Medium, SpaceGrotesk_600SemiBold, SpaceGrotesk_700Bold } from '@expo-google-fonts/space-grotesk';
3
+ import { Karla_400Regular, Karla_500Medium, Karla_600SemiBold, Karla_700Bold } from '@expo-google-fonts/karla';
4
+ import type { TextStyle } from 'react-native';
5
+
6
+ // Typography fonts — the SAME families the Flutter SDK uses via google_fonts:
7
+ // Space Grotesk for headings, Karla for body. Loaded at runtime through
8
+ // expo-font (already natively linked), so no native rebuild is needed. Each
9
+ // weight is its own RN font family, so we resolve family-by-weight below.
10
+
11
+ export const MYAZA_FONTS = {
12
+ SpaceGrotesk_500Medium,
13
+ SpaceGrotesk_600SemiBold,
14
+ SpaceGrotesk_700Bold,
15
+ Karla_400Regular,
16
+ Karla_500Medium,
17
+ Karla_600SemiBold,
18
+ Karla_700Bold,
19
+ };
20
+
21
+ /** Loads the Myaza fonts. Returns true once they're ready (system font until then). */
22
+ export function useMyazaFonts(): boolean {
23
+ const [loaded] = useFonts(MYAZA_FONTS);
24
+ return loaded;
25
+ }
26
+
27
+ /**
28
+ * Resolves the concrete RN font family for a (heading vs body, weight) pair.
29
+ * Returns `undefined` until fonts are loaded so text falls back to the system
30
+ * font + `fontWeight` without triggering an "unrecognized font" warning.
31
+ */
32
+ export function fontFamilyFor(
33
+ isHeading: boolean,
34
+ weight: TextStyle['fontWeight'],
35
+ loaded: boolean,
36
+ ): string | undefined {
37
+ if (!loaded) return undefined;
38
+ const w = String(weight ?? '400');
39
+ const bold = w === '700' || w === '800' || w === '900' || w === 'bold';
40
+ if (isHeading) {
41
+ if (bold) return 'SpaceGrotesk_700Bold';
42
+ if (w === '500') return 'SpaceGrotesk_500Medium';
43
+ return 'SpaceGrotesk_600SemiBold'; // 600 default for headings
44
+ }
45
+ if (bold) return 'Karla_700Bold';
46
+ if (w === '600') return 'Karla_600SemiBold';
47
+ if (w === '500') return 'Karla_500Medium';
48
+ return 'Karla_400Regular';
49
+ }
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { View, type StyleProp, type ViewStyle } from 'react-native';
3
+
4
+ import { radius } from '../../config/theme';
5
+ import { GlassSurface, supportsLiquidGlass } from './GlassSurface';
6
+
7
+ // GlassGroup — renders a set of header controls as a SINGLE Liquid Glass capsule
8
+ // on iOS 26 (one clean pill behind both icons) rather than two separate glass
9
+ // circles. The child icon buttons are passed `plain` so they don't draw their
10
+ // own glass — the capsule is the only glass surface, giving a crisp grouped look
11
+ // (like the iOS 26 navigation-bar control groups). On Android / iOS < 26 it's a
12
+ // plain transparent row.
13
+
14
+ export interface GlassGroupProps {
15
+ children: React.ReactNode;
16
+ style?: StyleProp<ViewStyle>;
17
+ }
18
+
19
+ export function GlassGroup({ children, style }: GlassGroupProps): React.ReactElement {
20
+ const row: ViewStyle = { flexDirection: 'row', alignItems: 'center' };
21
+
22
+ if (supportsLiquidGlass()) {
23
+ return (
24
+ <GlassSurface
25
+ glassStyle="regular"
26
+ interactive
27
+ style={[row, { borderRadius: radius.full, paddingHorizontal: 4 }, style]}
28
+ >
29
+ {children}
30
+ </GlassSurface>
31
+ );
32
+ }
33
+ return <View style={[row, style]}>{children}</View>;
34
+ }
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import { Platform, View, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { GlassView, isLiquidGlassAvailable, type GlassStyle } from 'expo-glass-effect';
4
+
5
+ import { useTheme } from '../runtime';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // GlassSurface — the single switch point for iOS 26 Liquid Glass.
9
+ //
10
+ // On iOS 26+ (where `isLiquidGlassAvailable()` is true) it renders a native
11
+ // `GlassView`; everywhere else (Android, iOS < 26) it falls back to a plain
12
+ // token-styled surface. Glass is purely additive — every screen looks correct
13
+ // without it. Used for the sheet shell, header, primary buttons, and ID cards.
14
+ // ---------------------------------------------------------------------------
15
+
16
+ const liquidGlass = Platform.OS === 'ios' && isLiquidGlassAvailable();
17
+
18
+ /** Whether the running device supports the Liquid Glass design. */
19
+ export function supportsLiquidGlass(): boolean {
20
+ return liquidGlass;
21
+ }
22
+
23
+ export interface GlassSurfaceProps {
24
+ children?: React.ReactNode;
25
+ style?: StyleProp<ViewStyle>;
26
+ /** Glass intensity on capable devices. */
27
+ glassStyle?: GlassStyle;
28
+ /** Optional tint laid over the glass (brand color, low alpha). */
29
+ tintColor?: string;
30
+ /** Whether the glass reacts to touch (for interactive chrome like buttons). */
31
+ interactive?: boolean;
32
+ /**
33
+ * Background color used on the NON-glass fallback path. Defaults to the
34
+ * theme's elevated surface (`backgroundSecondary`).
35
+ */
36
+ fallbackColor?: string;
37
+ }
38
+
39
+ export function GlassSurface({
40
+ children,
41
+ style,
42
+ glassStyle = 'regular',
43
+ tintColor,
44
+ interactive = false,
45
+ fallbackColor,
46
+ }: GlassSurfaceProps): React.ReactElement {
47
+ const { colors, mode } = useTheme();
48
+
49
+ if (liquidGlass) {
50
+ return (
51
+ <GlassView
52
+ style={style}
53
+ glassEffectStyle={glassStyle}
54
+ tintColor={tintColor}
55
+ isInteractive={interactive}
56
+ colorScheme={mode}
57
+ >
58
+ {children}
59
+ </GlassView>
60
+ );
61
+ }
62
+
63
+ return <View style={[{ backgroundColor: fallbackColor ?? colors.backgroundSecondary }, style]}>{children}</View>;
64
+ }
@@ -0,0 +1,93 @@
1
+ import React, { createContext, useContext, useMemo, useRef, useState } from 'react';
2
+ import { useStore } from 'zustand';
3
+
4
+ import { createKycStore, type KycState, type KycStore } from '../store/kycStore';
5
+ import { resolveColors, type MyazaColorScheme, type ThemeMode } from '../config/theme';
6
+ import type { MyazaKYCConfig } from '../types/config';
7
+ import { useMyazaFonts } from './fonts';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Runtime context — owns the per-instance zustand store + theme mode. Mirrors
11
+ // the Flutter SDK's ProviderScope + theme provider wiring.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ interface ThemeValue {
15
+ mode: ThemeMode;
16
+ colors: MyazaColorScheme;
17
+ setMode: (mode: ThemeMode) => void;
18
+ toggle: () => void;
19
+ /** True once Space Grotesk + Karla are loaded (system font until then). */
20
+ fontsLoaded: boolean;
21
+ }
22
+
23
+ interface RuntimeValue {
24
+ store: KycStore;
25
+ config: MyazaKYCConfig;
26
+ theme: ThemeValue;
27
+ }
28
+
29
+ const RuntimeContext = createContext<RuntimeValue | null>(null);
30
+
31
+ function useRuntime(): RuntimeValue {
32
+ const ctx = useContext(RuntimeContext);
33
+ if (!ctx) {
34
+ throw new Error('Myaza KYC components must be rendered inside <KycRuntimeProvider>.');
35
+ }
36
+ return ctx;
37
+ }
38
+
39
+ export function KycRuntimeProvider({
40
+ config,
41
+ children,
42
+ }: {
43
+ config: MyazaKYCConfig;
44
+ children: React.ReactNode;
45
+ }): React.ReactElement {
46
+ // The store is created once per provider instance (per modal launch).
47
+ const storeRef = useRef<KycStore | null>(null);
48
+ if (storeRef.current === null) {
49
+ storeRef.current = createKycStore(config);
50
+ }
51
+ const store = storeRef.current;
52
+
53
+ const [mode, setMode] = useState<ThemeMode>(config.appearance?.theme ?? 'light');
54
+ const colors = useMemo(() => resolveColors(mode, config.appearance), [mode, config.appearance]);
55
+ const fontsLoaded = useMyazaFonts();
56
+
57
+ const value = useMemo<RuntimeValue>(
58
+ () => ({
59
+ store,
60
+ config,
61
+ theme: {
62
+ mode,
63
+ colors,
64
+ setMode,
65
+ toggle: () => setMode((m) => (m === 'dark' ? 'light' : 'dark')),
66
+ fontsLoaded,
67
+ },
68
+ }),
69
+ [store, config, mode, colors, fontsLoaded],
70
+ );
71
+
72
+ return <RuntimeContext.Provider value={value}>{children}</RuntimeContext.Provider>;
73
+ }
74
+
75
+ /** Selects from the KYC store (re-renders only on selected-slice changes). */
76
+ export function useKyc<T>(selector: (state: KycState) => T): T {
77
+ return useStore(useRuntime().store, selector);
78
+ }
79
+
80
+ /** Returns the raw store (for actions / one-off reads). */
81
+ export function useKycStore(): KycStore {
82
+ return useRuntime().store;
83
+ }
84
+
85
+ /** The resolved SDK config (props + callbacks) for this launch. */
86
+ export function useKycConfig(): MyazaKYCConfig {
87
+ return useRuntime().config;
88
+ }
89
+
90
+ /** Theme colors + light/dark mode controls. */
91
+ export function useTheme(): ThemeValue {
92
+ return useRuntime().theme;
93
+ }