@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.
- package/KycSdkReactNative.podspec +32 -0
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/android/CMakeLists.txt +29 -0
- package/android/build.gradle +110 -0
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/cpp/cpp-adapter.cpp +12 -0
- package/android/src/main/java/co/myazahq/kyc/rn/MyazaFaceDetectorPackage.kt +36 -0
- package/android/src/main/java/co/myazahq/kyc/rn/MyazaStatusBarModule.kt +64 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +134 -0
- package/app.plugin.js +55 -0
- package/expo-module.config.json +6 -0
- package/ios/HybridMyazaFaceDetector.swift +233 -0
- package/package.json +84 -0
- package/react-native.config.js +23 -0
- package/src/MyazaKYC.tsx +235 -0
- package/src/__tests__/cardCrop.test.ts +39 -0
- package/src/__tests__/deviceMetadata.test.ts +34 -0
- package/src/__tests__/errors.test.ts +34 -0
- package/src/__tests__/flow.test.ts +61 -0
- package/src/__tests__/gestureDetector.test.ts +37 -0
- package/src/__tests__/liveness.test.ts +112 -0
- package/src/__tests__/resolveUrl.test.ts +64 -0
- package/src/__tests__/validators.test.ts +38 -0
- package/src/assets/liveness/Blink.gif +0 -0
- package/src/assets/liveness/Nod.gif +0 -0
- package/src/assets/liveness/Smile.gif +0 -0
- package/src/assets/liveness/Turn.gif +0 -0
- package/src/components/CameraPermissionView.tsx +116 -0
- package/src/components/CameraViewfinder.tsx +156 -0
- package/src/components/CountryFlag.tsx +52 -0
- package/src/components/DocumentCropper.tsx +325 -0
- package/src/components/GlassIconButton.tsx +92 -0
- package/src/components/Icon.tsx +125 -0
- package/src/components/KycFlow.tsx +205 -0
- package/src/components/KycSheet.tsx +224 -0
- package/src/components/MyazaAlert.tsx +57 -0
- package/src/components/MyazaButton.tsx +101 -0
- package/src/components/MyazaCard.tsx +48 -0
- package/src/components/MyazaInput.tsx +112 -0
- package/src/components/MyazaPulseLoader.tsx +71 -0
- package/src/components/StatusBarController.tsx +42 -0
- package/src/components/StepHeader.tsx +56 -0
- package/src/components/StepIndicator.tsx +74 -0
- package/src/components/Typography.tsx +69 -0
- package/src/components/fonts.ts +49 -0
- package/src/components/glass/GlassGroup.tsx +34 -0
- package/src/components/glass/GlassSurface.tsx +64 -0
- package/src/components/runtime.tsx +93 -0
- package/src/components/toast.tsx +154 -0
- package/src/components/useBranding.ts +27 -0
- package/src/components/useVideoRecorder.ts +122 -0
- package/src/config/captureSettings.ts +67 -0
- package/src/config/idTypes.ts +79 -0
- package/src/config/theme.ts +186 -0
- package/src/index.ts +54 -0
- package/src/liveness/challengeManager.ts +130 -0
- package/src/liveness/faceDetector.ts +79 -0
- package/src/liveness/gestureDetector.ts +80 -0
- package/src/liveness/speech.ts +66 -0
- package/src/liveness/types.ts +99 -0
- package/src/liveness/useLiveness.ts +484 -0
- package/src/liveness/visionCameraFaceDetector.ts +118 -0
- package/src/screens/ConsentStep.tsx +164 -0
- package/src/screens/DocumentCaptureStep.tsx +500 -0
- package/src/screens/IdInputStep.tsx +79 -0
- package/src/screens/IdTypeStep.tsx +142 -0
- package/src/screens/LivenessAvatar.tsx +69 -0
- package/src/screens/LivenessStep.tsx +615 -0
- package/src/screens/SubmittedStep.tsx +177 -0
- package/src/services/api.ts +291 -0
- package/src/services/cardCrop.ts +52 -0
- package/src/services/deviceMetadata.ts +185 -0
- package/src/services/errors.ts +92 -0
- package/src/services/mediaCompress.ts +129 -0
- package/src/services/resolveUrl.ts +98 -0
- package/src/services/retry.ts +70 -0
- package/src/services/validators.ts +103 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +44 -0
- package/src/store/kycStore.ts +288 -0
- package/src/store/serverConfig.ts +78 -0
- package/src/types/config.ts +239 -0
- package/src/types/country-flag-icons.d.ts +6 -0
- package/src/types/verification.ts +79 -0
- package/src/utils/platform.ts +23 -0
- package/src/utils/tokens.ts +10 -0
- 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
|
+
}
|