@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.
Files changed (46) hide show
  1. package/building-blocks-rn-macos.podspec +20 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.js +4 -0
  4. package/dist/src/api/ApiContext.d.ts +10 -0
  5. package/dist/src/api/ApiContext.js +10 -0
  6. package/dist/src/app/SudobilityAppRN.d.ts +13 -7
  7. package/dist/src/app/SudobilityAppRN.js +11 -8
  8. package/dist/src/components/error/ErrorBoundary.d.ts +40 -0
  9. package/dist/src/components/error/ErrorBoundary.js +85 -0
  10. package/dist/src/components/error/index.d.ts +2 -0
  11. package/dist/src/components/error/index.js +1 -0
  12. package/dist/src/components/footer/AppFooter.js +10 -1
  13. package/dist/src/components/header/AppHeader.d.ts +8 -0
  14. package/dist/src/components/header/AppHeader.js +3 -3
  15. package/dist/src/components/layout/AppScreenLayout.d.ts +7 -0
  16. package/dist/src/components/pages/AppSubscriptionPage.js +19 -5
  17. package/dist/src/components/pages/AppTextScreen.d.ts +8 -0
  18. package/dist/src/components/pages/AppTextScreen.js +2 -2
  19. package/dist/src/components/pages/LoginScreen.js +18 -3
  20. package/dist/src/components/settings/AppearanceSettings.js +18 -14
  21. package/dist/src/components/settings/LanguagePicker.js +20 -5
  22. package/dist/src/components/settings/SettingsListScreen.js +4 -4
  23. package/dist/src/components/subscription/SafeSubscriptionContext.js +9 -0
  24. package/dist/src/components/subscription/SubscriptionScreen.js +17 -4
  25. package/dist/src/components/toast/ToastProvider.d.ts +23 -0
  26. package/dist/src/components/toast/ToastProvider.js +46 -6
  27. package/dist/src/constants/languages.d.ts +8 -0
  28. package/dist/src/constants/languages.js +8 -0
  29. package/dist/src/i18n/index.d.ts +6 -3
  30. package/dist/src/i18n/index.js +6 -3
  31. package/dist/src/native/WebAuth.d.ts +3 -0
  32. package/dist/src/native/WebAuth.js +20 -0
  33. package/dist/src/theme/ThemeContext.d.ts +14 -0
  34. package/dist/src/theme/ThemeContext.js +35 -7
  35. package/dist/src/theme/colors.d.ts +6 -2
  36. package/dist/src/theme/colors.js +6 -2
  37. package/dist/src/theme/spacing.d.ts +5 -2
  38. package/dist/src/theme/spacing.js +5 -2
  39. package/dist/src/theme/typography.d.ts +6 -2
  40. package/dist/src/theme/typography.js +6 -2
  41. package/macos/WebAuthModule.h +4 -0
  42. package/macos/WebAuthModule.m +104 -0
  43. package/package.json +19 -11
  44. package/react-native.config.js +11 -0
  45. package/windows/WebAuthModule.cpp +245 -0
  46. 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
- marginRight: 12,
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
- marginLeft: 8,
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, children: loading === pkg.id ? (_jsx(ActivityIndicator, { color: '#ffffff', size: 'small' })) : (_jsx(Text, { style: styles.purchaseButtonText, children: labels.purchase ?? 'Subscribe' })) }))] }, pkg.id))) }), _jsx(Pressable, { style: [
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, 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 }))] }));
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
- import React, { createContext, useCallback, useContext, useRef, useState, } from 'react';
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-${++toastCounter}`;
46
+ const id = `toast-${++counterRef.current}`;
18
47
  setToasts(prev => [...prev, { id, message, type, duration }]);
19
48
  if (duration > 0) {
20
- setTimeout(() => removeToast(id), duration);
49
+ const timeoutId = setTimeout(() => {
50
+ removeToast(id);
51
+ }, duration);
52
+ timeoutsRef.current.set(id, timeoutId);
21
53
  }
22
54
  }, [removeToast]);
23
- return (_jsxs(ToastContext.Provider, { value: { addToast, removeToast }, children: [children, _jsx(View, { style: [styles.toastContainer, { top: insets.top + 8 }], pointerEvents: 'box-none', children: toasts.map(toast => (_jsx(ToastItem, { toast: toast, onDismiss: () => removeToast(toast.id) }, toast.id))) })] }));
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.
@@ -1,8 +1,11 @@
1
1
  /**
2
- * React Native i18n initialization.
2
+ * @fileoverview React Native i18n initialization using i18next.
3
3
  *
4
- * Uses react-native-localize for device language detection
5
- * and bundled translations (no HTTP backend).
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 {
@@ -1,8 +1,11 @@
1
1
  /**
2
- * React Native i18n initialization.
2
+ * @fileoverview React Native i18n initialization using i18next.
3
3
  *
4
- * Uses react-native-localize for device language detection
5
- * and bundled translations (no HTTP backend).
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,3 @@
1
+ export declare function authenticate(url: string, callbackURLScheme: string): Promise<string | null>;
2
+ export declare function generateCodeVerifier(): Promise<string>;
3
+ export declare function sha256Base64Url(input: string): Promise<string>;
@@ -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
- import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
3
- import { useColorScheme } from 'react-native';
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 building_blocks_rn.
3
- * Matches the web app's Tailwind color tokens.
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: {
@@ -1,6 +1,10 @@
1
1
  /**
2
- * Color palette for building_blocks_rn.
3
- * Matches the web app's Tailwind color tokens.
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 consistent layout.
3
- * Values in logical pixels.
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 consistent layout.
3
- * Values in logical pixels.
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,4 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface WebAuthModule : NSObject <RCTBridgeModule>
4
+ @end
@@ -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.9",
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": "echo 'No tests configured yet'",
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
- "@react-navigation/native": "^7.1.28",
83
- "@react-navigation/native-stack": "^7.10.1",
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"
@@ -0,0 +1,11 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ ios: null,
5
+ android: null,
6
+ macos: {
7
+ podspecPath: __dirname + '/building-blocks-rn-macos.podspec',
8
+ },
9
+ },
10
+ },
11
+ };