@sudobility/building_blocks_rn 0.0.9 → 0.0.12
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/building-blocks-rn-macos.podspec +20 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/src/api/ApiContext.d.ts +10 -0
- package/dist/src/api/ApiContext.js +10 -0
- package/dist/src/app/SudobilityAppRN.d.ts +13 -7
- package/dist/src/app/SudobilityAppRN.js +11 -8
- package/dist/src/components/error/ErrorBoundary.d.ts +40 -0
- package/dist/src/components/error/ErrorBoundary.js +85 -0
- package/dist/src/components/error/index.d.ts +2 -0
- package/dist/src/components/error/index.js +1 -0
- package/dist/src/components/footer/AppFooter.js +10 -1
- package/dist/src/components/header/AppHeader.d.ts +8 -0
- package/dist/src/components/header/AppHeader.js +3 -3
- package/dist/src/components/layout/AppScreenLayout.d.ts +7 -0
- package/dist/src/components/pages/AppSubscriptionPage.js +19 -5
- package/dist/src/components/pages/AppTextScreen.d.ts +8 -0
- package/dist/src/components/pages/AppTextScreen.js +2 -2
- package/dist/src/components/pages/LoginScreen.js +18 -3
- package/dist/src/components/settings/AppearanceSettings.js +18 -14
- package/dist/src/components/settings/LanguagePicker.js +20 -5
- package/dist/src/components/settings/SettingsListScreen.js +4 -4
- package/dist/src/components/subscription/SafeSubscriptionContext.js +9 -0
- package/dist/src/components/subscription/SubscriptionScreen.js +17 -4
- package/dist/src/components/toast/ToastProvider.d.ts +23 -0
- package/dist/src/components/toast/ToastProvider.js +46 -6
- package/dist/src/constants/languages.d.ts +8 -0
- package/dist/src/constants/languages.js +8 -0
- package/dist/src/i18n/index.d.ts +6 -3
- package/dist/src/i18n/index.js +6 -3
- package/dist/src/native/WebAuth.d.ts +3 -0
- package/dist/src/native/WebAuth.js +20 -0
- package/dist/src/theme/ThemeContext.d.ts +14 -0
- package/dist/src/theme/ThemeContext.js +35 -7
- package/dist/src/theme/colors.d.ts +6 -2
- package/dist/src/theme/colors.js +6 -2
- package/dist/src/theme/spacing.d.ts +5 -2
- package/dist/src/theme/spacing.js +5 -2
- package/dist/src/theme/typography.d.ts +6 -2
- package/dist/src/theme/typography.js +6 -2
- package/macos/WebAuthModule.h +4 -0
- package/macos/WebAuthModule.m +104 -0
- package/package.json +19 -11
- package/react-native.config.js +11 -0
- package/windows/WebAuthModule.cpp +245 -0
- package/windows/WebAuthModule.h +29 -0
|
@@ -12,12 +12,12 @@ export function SettingsListScreen({ sections, onSectionPress, title = 'Settings
|
|
|
12
12
|
});
|
|
13
13
|
onSectionPress(section.id);
|
|
14
14
|
};
|
|
15
|
-
return (_jsxs(ScrollView, { style: [styles.container, style], contentContainerStyle: styles.content, children: [_jsx(Text, { style: styles.title, children: title }), _jsx(View, { style: styles.sectionList, children: sections.map((section, index) => {
|
|
15
|
+
return (_jsxs(ScrollView, { style: [styles.container, style], contentContainerStyle: styles.content, children: [_jsx(Text, { style: styles.title, accessibilityRole: 'header', children: title }), _jsx(View, { style: styles.sectionList, accessibilityRole: 'list', children: sections.map((section, index) => {
|
|
16
16
|
const IconComponent = section.icon;
|
|
17
17
|
return (_jsxs(Pressable, { style: [
|
|
18
18
|
styles.sectionRow,
|
|
19
19
|
index < sections.length - 1 && styles.sectionRowBorder,
|
|
20
|
-
], onPress: () => handlePress(section), children: [IconComponent && (_jsx(View, { style: styles.iconContainer, children: _jsx(IconComponent, { size: 20, color: styles.iconColor.color }) })), _jsxs(View, { style: styles.sectionInfo, children: [_jsx(Text, { style: styles.sectionLabel, children: section.label }), section.description && (_jsx(Text, { style: styles.sectionDescription, children: section.description }))] }), _jsx(Text, { style: styles.chevron, children: '\u203A' })] }, section.id));
|
|
20
|
+
], onPress: () => handlePress(section), accessibilityRole: 'button', accessibilityLabel: `${section.label}${section.description ? `, ${section.description}` : ''}`, accessibilityHint: `Opens ${section.label} settings`, children: [IconComponent && (_jsx(View, { style: styles.iconContainer, children: _jsx(IconComponent, { size: 20, color: styles.iconColor.color }) })), _jsxs(View, { style: styles.sectionInfo, children: [_jsx(Text, { style: styles.sectionLabel, children: section.label }), section.description && (_jsx(Text, { style: styles.sectionDescription, children: section.description }))] }), _jsx(Text, { style: styles.chevron, children: '\u203A' })] }, section.id));
|
|
21
21
|
}) })] }));
|
|
22
22
|
}
|
|
23
23
|
const useStyles = createThemedStyles(colors => ({
|
|
@@ -56,7 +56,7 @@ const useStyles = createThemedStyles(colors => ({
|
|
|
56
56
|
backgroundColor: colors.surfaceSecondary,
|
|
57
57
|
alignItems: 'center',
|
|
58
58
|
justifyContent: 'center',
|
|
59
|
-
|
|
59
|
+
marginEnd: 12,
|
|
60
60
|
},
|
|
61
61
|
iconColor: {
|
|
62
62
|
color: colors.primary,
|
|
@@ -77,6 +77,6 @@ const useStyles = createThemedStyles(colors => ({
|
|
|
77
77
|
chevron: {
|
|
78
78
|
fontSize: 22,
|
|
79
79
|
color: colors.textMuted,
|
|
80
|
-
|
|
80
|
+
marginStart: 8,
|
|
81
81
|
},
|
|
82
82
|
}));
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Safe subscription context for React Native.
|
|
3
|
+
*
|
|
4
|
+
* Provides a context that returns a stub value (`isSubscribed: false`) when
|
|
5
|
+
* no provider wraps the tree. This allows components to read subscription
|
|
6
|
+
* state without crashing when a subscription provider is absent. Uses a
|
|
7
|
+
* default context value rather than null, so `useSafeSubscription()` never
|
|
8
|
+
* throws and never returns null.
|
|
9
|
+
*/
|
|
1
10
|
import { createContext, useContext } from 'react';
|
|
2
11
|
export const STUB_SUBSCRIPTION_VALUE = {
|
|
3
12
|
isSubscribed: false,
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Subscription plan selector screen for React Native.
|
|
4
|
+
*
|
|
5
|
+
* Simpler plan selector (no current status display) with package cards,
|
|
6
|
+
* purchase buttons, and a restore purchases flow. Uses the same
|
|
7
|
+
* purchase/restore callback pattern as AppSubscriptionPage.
|
|
8
|
+
*/
|
|
2
9
|
import { useState, useCallback } from 'react';
|
|
3
10
|
import { View, Text, Pressable, ScrollView, ActivityIndicator, } from 'react-native';
|
|
4
11
|
import { createThemedStyles } from '../../utils/styles';
|
|
@@ -52,16 +59,22 @@ export function SubscriptionScreen({ packages, labels = {}, formatters = {}, onP
|
|
|
52
59
|
setLoading(null);
|
|
53
60
|
}
|
|
54
61
|
}, [loading, onRestore, onRestoreSuccess, onError, onTrack]);
|
|
55
|
-
return (_jsxs(ScrollView, { style: [styles.container, style], contentContainerStyle: styles.content, children: [_jsx(Text, { style: styles.title, children: labels.title ?? 'Subscription' }), labels.subtitle && (_jsx(Text, { style: styles.subtitle, children: labels.subtitle })), _jsx(View, { style: styles.packageList, children: packages.map(pkg => (_jsxs(View, { style: [
|
|
62
|
+
return (_jsxs(ScrollView, { style: [styles.container, style], contentContainerStyle: styles.content, children: [_jsx(Text, { style: styles.title, accessibilityRole: 'header', children: labels.title ?? 'Subscription' }), labels.subtitle && (_jsx(Text, { style: styles.subtitle, children: labels.subtitle })), _jsx(View, { style: styles.packageList, accessibilityRole: 'list', children: packages.map(pkg => (_jsxs(View, { style: [
|
|
56
63
|
styles.packageCard,
|
|
57
64
|
pkg.isMostPopular && styles.packageCardPopular,
|
|
58
|
-
], children: [pkg.isMostPopular && (_jsx(View, { style: styles.popularBadge, children: _jsx(Text, { style: styles.popularBadgeText, children: labels.mostPopular ?? 'Most Popular' }) })), _jsx(Text, { style: styles.packageTitle, children: pkg.title }), pkg.description && (_jsx(Text, { style: styles.packageDescription, children: pkg.description })), _jsx(Text, { style: styles.packagePrice, children: formatPrice(pkg.price, pkg.currency) }), pkg.features && pkg.features.length > 0 && (_jsx(View, { style: styles.featureList, children: pkg.features.map((feature, i) => (_jsxs(Text, { style: styles.featureItem, children: ['\u2713', " ", feature] }, i))) })), pkg.isCurrent ? (_jsx(View, { style: styles.currentBadge, children: _jsx(Text, { style: styles.currentBadgeText, children: labels.currentlyActive ?? 'Current Plan' }) })) : (_jsx(Pressable, { style: [
|
|
65
|
+
], accessibilityRole: 'summary', accessibilityLabel: `${pkg.title}, ${formatPrice(pkg.price, pkg.currency)}${pkg.isMostPopular ? ', Most Popular' : ''}${pkg.isCurrent ? ', Current Plan' : ''}`, children: [pkg.isMostPopular && (_jsx(View, { style: styles.popularBadge, children: _jsx(Text, { style: styles.popularBadgeText, children: labels.mostPopular ?? 'Most Popular' }) })), _jsx(Text, { style: styles.packageTitle, children: pkg.title }), pkg.description && (_jsx(Text, { style: styles.packageDescription, children: pkg.description })), _jsx(Text, { style: styles.packagePrice, children: formatPrice(pkg.price, pkg.currency) }), pkg.features && pkg.features.length > 0 && (_jsx(View, { style: styles.featureList, children: pkg.features.map((feature, i) => (_jsxs(Text, { style: styles.featureItem, children: ['\u2713', " ", feature] }, i))) })), pkg.isCurrent ? (_jsx(View, { style: styles.currentBadge, children: _jsx(Text, { style: styles.currentBadgeText, children: labels.currentlyActive ?? 'Current Plan' }) })) : (_jsx(Pressable, { style: [
|
|
59
66
|
styles.purchaseButton,
|
|
60
67
|
loading === pkg.id && styles.purchaseButtonDisabled,
|
|
61
|
-
], onPress: () => handlePurchase(pkg), disabled: loading !== null,
|
|
68
|
+
], onPress: () => handlePurchase(pkg), disabled: loading !== null, accessibilityRole: 'button', accessibilityLabel: `${labels.purchase ?? 'Subscribe'} to ${pkg.title}`, accessibilityState: {
|
|
69
|
+
disabled: loading !== null,
|
|
70
|
+
busy: loading === pkg.id,
|
|
71
|
+
}, children: loading === pkg.id ? (_jsx(ActivityIndicator, { color: '#ffffff', size: 'small' })) : (_jsx(Text, { style: styles.purchaseButtonText, children: labels.purchase ?? 'Subscribe' })) }))] }, pkg.id))) }), _jsx(Pressable, { style: [
|
|
62
72
|
styles.restoreButton,
|
|
63
73
|
loading === 'restore' && styles.restoreButtonDisabled,
|
|
64
|
-
], onPress: handleRestore, disabled: loading !== null,
|
|
74
|
+
], onPress: handleRestore, disabled: loading !== null, accessibilityRole: 'button', accessibilityLabel: labels.restore ?? 'Restore Purchases', accessibilityState: {
|
|
75
|
+
disabled: loading !== null,
|
|
76
|
+
busy: loading === 'restore',
|
|
77
|
+
}, children: loading === 'restore' ? (_jsx(ActivityIndicator, { color: styles.restoreText.color, size: 'small' })) : (_jsx(Text, { style: styles.restoreText, children: labels.restore ?? 'Restore Purchases' })) }), labels.restoreDescription && (_jsx(Text, { style: styles.restoreDescription, children: labels.restoreDescription }))] }));
|
|
65
78
|
}
|
|
66
79
|
const useStyles = createThemedStyles(colors => ({
|
|
67
80
|
container: {
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Animated toast notification provider for React Native.
|
|
3
|
+
*
|
|
4
|
+
* Provides a context-based toast system with slide-down + fade-in animations.
|
|
5
|
+
* Toasts are positioned at the top of the screen, respecting safe area insets.
|
|
6
|
+
* Supports four toast types (success, error, warning, info) with configurable
|
|
7
|
+
* auto-dismiss duration and tap-to-dismiss.
|
|
8
|
+
*
|
|
9
|
+
* @platform ios - Safe area insets from notch/dynamic island are respected.
|
|
10
|
+
* @platform android - Safe area insets from status bar are respected.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* // Wrap your app
|
|
15
|
+
* <ToastProvider>
|
|
16
|
+
* <App />
|
|
17
|
+
* </ToastProvider>
|
|
18
|
+
*
|
|
19
|
+
* // Show a toast from any child
|
|
20
|
+
* const { addToast } = useToast();
|
|
21
|
+
* addToast('Saved!', 'success');
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
1
24
|
import React from 'react';
|
|
2
25
|
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
|
3
26
|
export interface Toast {
|
|
@@ -1,26 +1,66 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Animated toast notification provider for React Native.
|
|
4
|
+
*
|
|
5
|
+
* Provides a context-based toast system with slide-down + fade-in animations.
|
|
6
|
+
* Toasts are positioned at the top of the screen, respecting safe area insets.
|
|
7
|
+
* Supports four toast types (success, error, warning, info) with configurable
|
|
8
|
+
* auto-dismiss duration and tap-to-dismiss.
|
|
9
|
+
*
|
|
10
|
+
* @platform ios - Safe area insets from notch/dynamic island are respected.
|
|
11
|
+
* @platform android - Safe area insets from status bar are respected.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* // Wrap your app
|
|
16
|
+
* <ToastProvider>
|
|
17
|
+
* <App />
|
|
18
|
+
* </ToastProvider>
|
|
19
|
+
*
|
|
20
|
+
* // Show a toast from any child
|
|
21
|
+
* const { addToast } = useToast();
|
|
22
|
+
* addToast('Saved!', 'success');
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import React, { createContext, useCallback, useContext, useEffect, useRef, useState, } from 'react';
|
|
3
26
|
import { View, Text, Animated, Pressable } from 'react-native';
|
|
4
27
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
5
28
|
import { useTheme } from '../../theme/ThemeContext';
|
|
6
29
|
import { createThemedStyles } from '../../utils/styles';
|
|
7
30
|
const ToastContext = createContext(null);
|
|
8
|
-
let toastCounter = 0;
|
|
9
31
|
export function ToastProvider({ children }) {
|
|
10
32
|
const [toasts, setToasts] = useState([]);
|
|
11
33
|
const insets = useSafeAreaInsets();
|
|
12
34
|
const styles = useToastStyles();
|
|
35
|
+
const counterRef = useRef(0);
|
|
36
|
+
const timeoutsRef = useRef(new Map());
|
|
13
37
|
const removeToast = useCallback((id) => {
|
|
14
38
|
setToasts(prev => prev.filter(t => t.id !== id));
|
|
39
|
+
const timeoutId = timeoutsRef.current.get(id);
|
|
40
|
+
if (timeoutId) {
|
|
41
|
+
clearTimeout(timeoutId);
|
|
42
|
+
timeoutsRef.current.delete(id);
|
|
43
|
+
}
|
|
15
44
|
}, []);
|
|
16
45
|
const addToast = useCallback((message, type = 'info', duration = 3000) => {
|
|
17
|
-
const id = `toast-${++
|
|
46
|
+
const id = `toast-${++counterRef.current}`;
|
|
18
47
|
setToasts(prev => [...prev, { id, message, type, duration }]);
|
|
19
48
|
if (duration > 0) {
|
|
20
|
-
setTimeout(() =>
|
|
49
|
+
const timeoutId = setTimeout(() => {
|
|
50
|
+
removeToast(id);
|
|
51
|
+
}, duration);
|
|
52
|
+
timeoutsRef.current.set(id, timeoutId);
|
|
21
53
|
}
|
|
22
54
|
}, [removeToast]);
|
|
23
|
-
|
|
55
|
+
// Clean up all timeouts on unmount
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const timeouts = timeoutsRef.current;
|
|
58
|
+
return () => {
|
|
59
|
+
timeouts.forEach(timeoutId => clearTimeout(timeoutId));
|
|
60
|
+
timeouts.clear();
|
|
61
|
+
};
|
|
62
|
+
}, []);
|
|
63
|
+
return (_jsxs(ToastContext.Provider, { value: { addToast, removeToast }, children: [children, _jsx(View, { style: [styles.toastContainer, { top: insets.top + 8 }], pointerEvents: 'box-none', accessibilityRole: 'alert', accessibilityLiveRegion: 'polite', children: toasts.map(toast => (_jsx(ToastItem, { toast: toast, onDismiss: () => removeToast(toast.id) }, toast.id))) })] }));
|
|
24
64
|
}
|
|
25
65
|
function ToastItem({ toast, onDismiss, }) {
|
|
26
66
|
const { colors } = useTheme();
|
|
@@ -50,7 +90,7 @@ function ToastItem({ toast, onDismiss, }) {
|
|
|
50
90
|
return (_jsx(Animated.View, { style: [
|
|
51
91
|
styles.toast,
|
|
52
92
|
{ backgroundColor: bgColor, opacity, transform: [{ translateY }] },
|
|
53
|
-
], children: _jsx(Pressable, { style: styles.toastContent, onPress: onDismiss, children: _jsx(Text, { style: styles.toastText, children: toast.message }) }) }));
|
|
93
|
+
], accessibilityRole: 'alert', accessibilityLabel: `${toast.type}: ${toast.message}`, children: _jsx(Pressable, { style: styles.toastContent, onPress: onDismiss, accessibilityRole: 'button', accessibilityLabel: `Dismiss notification: ${toast.message}`, children: _jsx(Text, { style: styles.toastText, children: toast.message }) }) }));
|
|
54
94
|
}
|
|
55
95
|
/**
|
|
56
96
|
* Hook to access toast functionality.
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Language configuration constants for React Native i18n.
|
|
3
|
+
*
|
|
4
|
+
* Defines the `LanguageConfig` type, a default set of 16 supported languages
|
|
5
|
+
* with emoji flags, the `RTL_LANGUAGES` array (currently Arabic), and an
|
|
6
|
+
* `isRTL()` helper function for checking if a language code uses
|
|
7
|
+
* right-to-left text direction.
|
|
8
|
+
*/
|
|
1
9
|
export interface LanguageConfig {
|
|
2
10
|
code: string;
|
|
3
11
|
name: string;
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Language configuration constants for React Native i18n.
|
|
3
|
+
*
|
|
4
|
+
* Defines the `LanguageConfig` type, a default set of 16 supported languages
|
|
5
|
+
* with emoji flags, the `RTL_LANGUAGES` array (currently Arabic), and an
|
|
6
|
+
* `isRTL()` helper function for checking if a language code uses
|
|
7
|
+
* right-to-left text direction.
|
|
8
|
+
*/
|
|
1
9
|
/**
|
|
2
10
|
* Default set of 16 supported languages with their flags.
|
|
3
11
|
* Apps can override this list by passing their own languages prop.
|
package/dist/src/i18n/index.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* React Native i18n initialization.
|
|
2
|
+
* @fileoverview React Native i18n initialization using i18next.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Provides `initializeI18nRN()` for configuring the i18n instance with
|
|
5
|
+
* bundled translations (no HTTP backend). Uses `react-native-localize`
|
|
6
|
+
* (optional peer dependency) for device language detection, falling back
|
|
7
|
+
* to English. Safe to call multiple times -- only initializes once.
|
|
8
|
+
* Includes Chinese fallback chains: `zh -> zh -> en`, `zh-hant -> zh-hant -> zh -> en`.
|
|
6
9
|
*/
|
|
7
10
|
import i18n from 'i18next';
|
|
8
11
|
export interface I18nConfig {
|
package/dist/src/i18n/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* React Native i18n initialization.
|
|
2
|
+
* @fileoverview React Native i18n initialization using i18next.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Provides `initializeI18nRN()` for configuring the i18n instance with
|
|
5
|
+
* bundled translations (no HTTP backend). Uses `react-native-localize`
|
|
6
|
+
* (optional peer dependency) for device language detection, falling back
|
|
7
|
+
* to English. Safe to call multiple times -- only initializes once.
|
|
8
|
+
* Includes Chinese fallback chains: `zh -> zh -> en`, `zh-hant -> zh-hant -> zh -> en`.
|
|
6
9
|
*/
|
|
7
10
|
import i18n from 'i18next';
|
|
8
11
|
import { initReactI18next } from 'react-i18next';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NativeModules, Platform } from 'react-native';
|
|
2
|
+
const { WebAuthModule } = NativeModules;
|
|
3
|
+
export async function authenticate(url, callbackURLScheme) {
|
|
4
|
+
if ((Platform.OS === 'macos' || Platform.OS === 'windows') && WebAuthModule) {
|
|
5
|
+
return WebAuthModule.authenticate(url, callbackURLScheme);
|
|
6
|
+
}
|
|
7
|
+
throw new Error(`Web auth not implemented for ${Platform.OS}`);
|
|
8
|
+
}
|
|
9
|
+
export async function generateCodeVerifier() {
|
|
10
|
+
if ((Platform.OS === 'macos' || Platform.OS === 'windows') && WebAuthModule) {
|
|
11
|
+
return WebAuthModule.generateCodeVerifier();
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`Code verifier generation not implemented for ${Platform.OS}`);
|
|
14
|
+
}
|
|
15
|
+
export async function sha256Base64Url(input) {
|
|
16
|
+
if ((Platform.OS === 'macos' || Platform.OS === 'windows') && WebAuthModule) {
|
|
17
|
+
return WebAuthModule.sha256(input);
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`SHA-256 not implemented for ${Platform.OS}`);
|
|
20
|
+
}
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Theme context and provider for React Native.
|
|
3
|
+
*
|
|
4
|
+
* Manages light/dark mode with automatic system detection via `useColorScheme()`.
|
|
5
|
+
* Persists user preference to AsyncStorage and delays rendering until the
|
|
6
|
+
* persisted theme is loaded (prevents theme flash on startup).
|
|
7
|
+
*
|
|
8
|
+
* Includes a subtle fade animation (250ms) when switching between light and dark
|
|
9
|
+
* modes to provide a smooth visual transition.
|
|
10
|
+
*
|
|
11
|
+
* Provides dual hooks:
|
|
12
|
+
* - `useTheme()` - throws if no ThemeProvider is present
|
|
13
|
+
* - `useThemeSafe()` - returns null if no ThemeProvider is present
|
|
14
|
+
*/
|
|
1
15
|
import React from 'react';
|
|
2
16
|
import { Theme } from '../types';
|
|
3
17
|
import { type ThemeColors } from './colors';
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Theme context and provider for React Native.
|
|
4
|
+
*
|
|
5
|
+
* Manages light/dark mode with automatic system detection via `useColorScheme()`.
|
|
6
|
+
* Persists user preference to AsyncStorage and delays rendering until the
|
|
7
|
+
* persisted theme is loaded (prevents theme flash on startup).
|
|
8
|
+
*
|
|
9
|
+
* Includes a subtle fade animation (250ms) when switching between light and dark
|
|
10
|
+
* modes to provide a smooth visual transition.
|
|
11
|
+
*
|
|
12
|
+
* Provides dual hooks:
|
|
13
|
+
* - `useTheme()` - throws if no ThemeProvider is present
|
|
14
|
+
* - `useThemeSafe()` - returns null if no ThemeProvider is present
|
|
15
|
+
*/
|
|
16
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
17
|
+
import { Animated, useColorScheme } from 'react-native';
|
|
4
18
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
5
19
|
import { Theme } from '../types';
|
|
6
20
|
import { lightColors, darkColors } from './colors';
|
|
@@ -10,6 +24,7 @@ export function ThemeProvider({ children, initialTheme, storageKeyPrefix, }) {
|
|
|
10
24
|
const systemColorScheme = useColorScheme();
|
|
11
25
|
const [theme, setThemeState] = useState(initialTheme ?? Theme.SYSTEM);
|
|
12
26
|
const [loaded, setLoaded] = useState(false);
|
|
27
|
+
const fadeAnim = useRef(new Animated.Value(1)).current;
|
|
13
28
|
const storageKey = storageKeyPrefix
|
|
14
29
|
? `${storageKeyPrefix}/${THEME_STORAGE_KEY}`
|
|
15
30
|
: THEME_STORAGE_KEY;
|
|
@@ -22,16 +37,29 @@ export function ThemeProvider({ children, initialTheme, storageKeyPrefix, }) {
|
|
|
22
37
|
setLoaded(true);
|
|
23
38
|
});
|
|
24
39
|
}, [storageKey]);
|
|
25
|
-
const setTheme = useCallback((newTheme) => {
|
|
26
|
-
setThemeState(newTheme);
|
|
27
|
-
AsyncStorage.setItem(storageKey, newTheme);
|
|
28
|
-
}, [storageKey]);
|
|
29
40
|
const resolvedTheme = useMemo(() => {
|
|
30
41
|
if (theme === Theme.SYSTEM) {
|
|
31
42
|
return systemColorScheme === 'dark' ? 'dark' : 'light';
|
|
32
43
|
}
|
|
33
44
|
return theme === Theme.DARK ? 'dark' : 'light';
|
|
34
45
|
}, [theme, systemColorScheme]);
|
|
46
|
+
// Track previous resolved theme for animation
|
|
47
|
+
const prevResolvedThemeRef = useRef(resolvedTheme);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (loaded && prevResolvedThemeRef.current !== resolvedTheme) {
|
|
50
|
+
prevResolvedThemeRef.current = resolvedTheme;
|
|
51
|
+
fadeAnim.setValue(0.7);
|
|
52
|
+
Animated.timing(fadeAnim, {
|
|
53
|
+
toValue: 1,
|
|
54
|
+
duration: 250,
|
|
55
|
+
useNativeDriver: true,
|
|
56
|
+
}).start();
|
|
57
|
+
}
|
|
58
|
+
}, [resolvedTheme, loaded, fadeAnim]);
|
|
59
|
+
const setTheme = useCallback((newTheme) => {
|
|
60
|
+
setThemeState(newTheme);
|
|
61
|
+
AsyncStorage.setItem(storageKey, newTheme);
|
|
62
|
+
}, [storageKey]);
|
|
35
63
|
const colors = useMemo(() => (resolvedTheme === 'dark' ? darkColors : lightColors), [resolvedTheme]);
|
|
36
64
|
const value = useMemo(() => ({
|
|
37
65
|
theme,
|
|
@@ -44,7 +72,7 @@ export function ThemeProvider({ children, initialTheme, storageKeyPrefix, }) {
|
|
|
44
72
|
if (!loaded) {
|
|
45
73
|
return null;
|
|
46
74
|
}
|
|
47
|
-
return (_jsx(ThemeContext.Provider, { value: value, children: children }));
|
|
75
|
+
return (_jsx(ThemeContext.Provider, { value: value, children: _jsx(Animated.View, { style: { flex: 1, opacity: fadeAnim }, children: children }) }));
|
|
48
76
|
}
|
|
49
77
|
/**
|
|
50
78
|
* Hook to access theme context.
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Color palette for
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Color palette and theme color definitions for React Native.
|
|
3
|
+
*
|
|
4
|
+
* Defines the color palette (matching Tailwind CSS color tokens), the
|
|
5
|
+
* `ThemeColors` interface (20 properties), and both light and dark theme
|
|
6
|
+
* color objects. Light mode uses white/light-gray surfaces; dark mode uses
|
|
7
|
+
* gray-800/900 surfaces with inverted text colors.
|
|
4
8
|
*/
|
|
5
9
|
export declare const palette: {
|
|
6
10
|
readonly primary: {
|
package/dist/src/theme/colors.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Color palette for
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Color palette and theme color definitions for React Native.
|
|
3
|
+
*
|
|
4
|
+
* Defines the color palette (matching Tailwind CSS color tokens), the
|
|
5
|
+
* `ThemeColors` interface (20 properties), and both light and dark theme
|
|
6
|
+
* color objects. Light mode uses white/light-gray surfaces; dark mode uses
|
|
7
|
+
* gray-800/900 surfaces with inverted text colors.
|
|
4
8
|
*/
|
|
5
9
|
export const palette = {
|
|
6
10
|
primary: {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Spacing scale for
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Spacing scale constants for React Native layout.
|
|
3
|
+
*
|
|
4
|
+
* Provides a consistent spacing scale (xs through 5xl) with values in
|
|
5
|
+
* logical pixels, used throughout all building_blocks_rn components
|
|
6
|
+
* for margins, paddings, and gaps.
|
|
4
7
|
*/
|
|
5
8
|
export declare const spacing: {
|
|
6
9
|
readonly xs: 4;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Spacing scale for
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Spacing scale constants for React Native layout.
|
|
3
|
+
*
|
|
4
|
+
* Provides a consistent spacing scale (xs through 5xl) with values in
|
|
5
|
+
* logical pixels, used throughout all building_blocks_rn components
|
|
6
|
+
* for margins, paddings, and gaps.
|
|
4
7
|
*/
|
|
5
8
|
export const spacing = {
|
|
6
9
|
xs: 4,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { FontSize } from '../types';
|
|
2
1
|
/**
|
|
3
|
-
* Typography scale.
|
|
2
|
+
* @fileoverview Typography scale and font utilities for React Native.
|
|
3
|
+
*
|
|
4
|
+
* Defines font sizes (xs through 4xl), font weights (normal through bold),
|
|
5
|
+
* and a `getFontSizeMultiplier()` utility that maps the `FontSize` enum
|
|
6
|
+
* to a numeric scale factor (0.875 for small, 1.0 for medium, 1.125 for large).
|
|
4
7
|
*/
|
|
8
|
+
import { FontSize } from '../types';
|
|
5
9
|
export declare const fontSizes: {
|
|
6
10
|
readonly xs: 12;
|
|
7
11
|
readonly sm: 14;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { FontSize } from '../types';
|
|
2
1
|
/**
|
|
3
|
-
* Typography scale.
|
|
2
|
+
* @fileoverview Typography scale and font utilities for React Native.
|
|
3
|
+
*
|
|
4
|
+
* Defines font sizes (xs through 4xl), font weights (normal through bold),
|
|
5
|
+
* and a `getFontSizeMultiplier()` utility that maps the `FontSize` enum
|
|
6
|
+
* to a numeric scale factor (0.875 for small, 1.0 for medium, 1.125 for large).
|
|
4
7
|
*/
|
|
8
|
+
import { FontSize } from '../types';
|
|
5
9
|
export const fontSizes = {
|
|
6
10
|
xs: 12,
|
|
7
11
|
sm: 14,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#import "WebAuthModule.h"
|
|
2
|
+
#import <AuthenticationServices/AuthenticationServices.h>
|
|
3
|
+
#import <CommonCrypto/CommonDigest.h>
|
|
4
|
+
#import <Security/Security.h>
|
|
5
|
+
|
|
6
|
+
@interface WebAuthModule () <ASWebAuthenticationPresentationContextProviding>
|
|
7
|
+
@property (nonatomic, strong) ASWebAuthenticationSession *authSession;
|
|
8
|
+
@end
|
|
9
|
+
|
|
10
|
+
@implementation WebAuthModule
|
|
11
|
+
|
|
12
|
+
RCT_EXPORT_MODULE();
|
|
13
|
+
|
|
14
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
15
|
+
return YES;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
RCT_EXPORT_METHOD(authenticate:(NSString *)urlString
|
|
19
|
+
callbackURLScheme:(NSString *)callbackURLScheme
|
|
20
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
21
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
22
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
23
|
+
NSURL *authURL = [NSURL URLWithString:urlString];
|
|
24
|
+
if (!authURL) {
|
|
25
|
+
reject(@"INVALID_URL", @"Invalid authentication URL", nil);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
self.authSession =
|
|
30
|
+
[[ASWebAuthenticationSession alloc]
|
|
31
|
+
initWithURL:authURL
|
|
32
|
+
callbackURLScheme:callbackURLScheme
|
|
33
|
+
completionHandler:^(NSURL * _Nullable callbackURL,
|
|
34
|
+
NSError * _Nullable error) {
|
|
35
|
+
self.authSession = nil;
|
|
36
|
+
if (error) {
|
|
37
|
+
if (error.code == ASWebAuthenticationSessionErrorCodeCanceledLogin) {
|
|
38
|
+
resolve([NSNull null]);
|
|
39
|
+
} else {
|
|
40
|
+
reject(@"AUTH_ERROR", error.localizedDescription, error);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (callbackURL) {
|
|
45
|
+
resolve(callbackURL.absoluteString);
|
|
46
|
+
} else {
|
|
47
|
+
resolve([NSNull null]);
|
|
48
|
+
}
|
|
49
|
+
}];
|
|
50
|
+
|
|
51
|
+
self.authSession.presentationContextProvider = self;
|
|
52
|
+
self.authSession.prefersEphemeralWebBrowserSession = NO;
|
|
53
|
+
|
|
54
|
+
if (![self.authSession start]) {
|
|
55
|
+
self.authSession = nil;
|
|
56
|
+
reject(@"SESSION_ERROR",
|
|
57
|
+
@"Failed to start authentication session", nil);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
RCT_EXPORT_METHOD(generateCodeVerifier:(RCTPromiseResolveBlock)resolve
|
|
63
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
64
|
+
uint8_t randomBytes[32];
|
|
65
|
+
OSStatus status = SecRandomCopyBytes(kSecRandomDefault, 32, randomBytes);
|
|
66
|
+
if (status != errSecSuccess) {
|
|
67
|
+
reject(@"RANDOM_ERROR", @"Failed to generate random bytes", nil);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
NSData *data = [NSData dataWithBytes:randomBytes length:32];
|
|
72
|
+
NSString *base64 = [data base64EncodedStringWithOptions:0];
|
|
73
|
+
NSString *base64url = base64;
|
|
74
|
+
base64url = [base64url stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
|
|
75
|
+
base64url = [base64url stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
|
|
76
|
+
base64url = [base64url stringByReplacingOccurrencesOfString:@"=" withString:@""];
|
|
77
|
+
resolve(base64url);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
RCT_EXPORT_METHOD(sha256:(NSString *)input
|
|
81
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
82
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
83
|
+
NSData *data = [input dataUsingEncoding:NSUTF8StringEncoding];
|
|
84
|
+
uint8_t digest[CC_SHA256_DIGEST_LENGTH];
|
|
85
|
+
CC_SHA256(data.bytes, (CC_LONG)data.length, digest);
|
|
86
|
+
|
|
87
|
+
// Base64url encode
|
|
88
|
+
NSData *hashData = [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH];
|
|
89
|
+
NSString *base64 = [hashData base64EncodedStringWithOptions:0];
|
|
90
|
+
NSString *base64url = base64;
|
|
91
|
+
base64url = [base64url stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
|
|
92
|
+
base64url = [base64url stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
|
|
93
|
+
base64url = [base64url stringByReplacingOccurrencesOfString:@"=" withString:@""];
|
|
94
|
+
resolve(base64url);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#pragma mark - ASWebAuthenticationPresentationContextProviding
|
|
98
|
+
|
|
99
|
+
- (ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:
|
|
100
|
+
(ASWebAuthenticationSession *)session {
|
|
101
|
+
return NSApp.keyWindow ?: NSApp.windows.firstObject;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@end
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sudobility/building_blocks_rn",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Higher-level shared UI building blocks for Sudobility React Native apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|
|
23
|
+
"macos",
|
|
24
|
+
"windows",
|
|
25
|
+
"react-native.config.js",
|
|
26
|
+
"building-blocks-rn-macos.podspec",
|
|
23
27
|
"README.md"
|
|
24
28
|
],
|
|
25
29
|
"scripts": {
|
|
@@ -27,7 +31,7 @@
|
|
|
27
31
|
"dev": "tsc -p tsconfig.build.json --watch",
|
|
28
32
|
"clean": "rm -rf dist",
|
|
29
33
|
"typecheck": "tsc --noEmit",
|
|
30
|
-
"test": "
|
|
34
|
+
"test": "vitest run",
|
|
31
35
|
"lint": "eslint . --ext ts,tsx --ignore-pattern 'dist' --ignore-pattern 'node_modules' --report-unused-disable-directives --max-warnings 0",
|
|
32
36
|
"lint:fix": "eslint . --ext ts,tsx --ignore-pattern 'dist' --ignore-pattern 'node_modules' --fix",
|
|
33
37
|
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json}\"",
|
|
@@ -66,6 +70,13 @@
|
|
|
66
70
|
},
|
|
67
71
|
"devDependencies": {
|
|
68
72
|
"@eslint/js": "^9.38.0",
|
|
73
|
+
"@react-native-async-storage/async-storage": "2.2.0",
|
|
74
|
+
"@react-navigation/native": "^7.1.28",
|
|
75
|
+
"@react-navigation/native-stack": "^7.10.1",
|
|
76
|
+
"@sudobility/types": "^1.9.55",
|
|
77
|
+
"@tanstack/react-query": "^5.90.19",
|
|
78
|
+
"@types/react": "~19.1.0",
|
|
79
|
+
"@types/react-dom": "^19.2.3",
|
|
69
80
|
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
|
70
81
|
"@typescript-eslint/parser": "^8.44.1",
|
|
71
82
|
"eslint": "^9.38.0",
|
|
@@ -73,19 +84,16 @@
|
|
|
73
84
|
"eslint-plugin-prettier": "^5.5.4",
|
|
74
85
|
"eslint-plugin-react-hooks": "^7.0.0",
|
|
75
86
|
"eslint-plugin-react-refresh": "^0.4.0",
|
|
87
|
+
"i18next": "^25.8.0",
|
|
88
|
+
"jsdom": "^28.1.0",
|
|
76
89
|
"prettier": "^3.6.2",
|
|
77
|
-
"@types/react": "~19.1.0",
|
|
78
|
-
"typescript": "~5.9.2",
|
|
79
90
|
"react": "19.1.0",
|
|
91
|
+
"react-dom": "19.1.0",
|
|
92
|
+
"react-i18next": "^16.5.3",
|
|
80
93
|
"react-native": "0.81.5",
|
|
81
94
|
"react-native-safe-area-context": "~5.6.0",
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"@react-native-async-storage/async-storage": "2.2.0",
|
|
85
|
-
"i18next": "^25.8.0",
|
|
86
|
-
"react-i18next": "^16.5.3",
|
|
87
|
-
"@tanstack/react-query": "^5.90.19",
|
|
88
|
-
"@sudobility/types": "^1.9.54"
|
|
95
|
+
"typescript": "~5.9.2",
|
|
96
|
+
"vitest": "^4.0.18"
|
|
89
97
|
},
|
|
90
98
|
"publishConfig": {
|
|
91
99
|
"access": "public"
|