@sudobility/building_blocks_rn 0.0.9 → 0.0.11

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
@@ -0,0 +1,20 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'building-blocks-rn-macos'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.homepage = 'https://github.com/sudobility/building_blocks_rn'
10
+ s.license = package['license']
11
+ s.author = 'Sudobility'
12
+ s.source = { :git => 'https://github.com/sudobility/building_blocks_rn.git', :tag => s.version }
13
+
14
+ s.osx.deployment_target = '14.0'
15
+
16
+ s.source_files = 'macos/**/*.{h,m,mm}'
17
+ s.frameworks = 'AuthenticationServices', 'Security'
18
+
19
+ s.dependency 'React-Core'
20
+ end
package/dist/index.d.ts CHANGED
@@ -17,5 +17,8 @@ export { useResponsive } from './src/hooks';
17
17
  export type { ResponsiveInfo } from './src/hooks';
18
18
  export { ToastProvider, useToast } from './src/components/toast';
19
19
  export type { Toast, ToastType } from './src/components/toast';
20
+ export { ErrorBoundary } from './src/components/error';
21
+ export type { ErrorBoundaryProps } from './src/components/error';
20
22
  export { initializeI18nRN, getI18n, i18n } from './src/i18n';
21
23
  export type { I18nConfig } from './src/i18n';
24
+ export { authenticate as webAuthAuthenticate, generateCodeVerifier as webAuthGenerateCodeVerifier, sha256Base64Url as webAuthSha256Base64Url, } from './src/native/WebAuth';
package/dist/index.js CHANGED
@@ -21,5 +21,9 @@ export { createThemedStyles } from './src/utils';
21
21
  export { useResponsive } from './src/hooks';
22
22
  // Toast
23
23
  export { ToastProvider, useToast } from './src/components/toast';
24
+ // Error Boundary
25
+ export { ErrorBoundary } from './src/components/error';
24
26
  // i18n
25
27
  export { initializeI18nRN, getI18n, i18n } from './src/i18n';
28
+ // Native modules (desktop only)
29
+ export { authenticate as webAuthAuthenticate, generateCodeVerifier as webAuthGenerateCodeVerifier, sha256Base64Url as webAuthSha256Base64Url, } from './src/native/WebAuth';
@@ -1,3 +1,13 @@
1
+ /**
2
+ * @fileoverview API context and provider for React Native (Firebase entry only).
3
+ *
4
+ * Provides a network client context with typed HTTP methods (get, post, put, delete),
5
+ * authentication state (token, userId, isReady, isLoading), and optional token
6
+ * refresh. Uses a default fetch-based client with JSON content type, with support
7
+ * for custom network client overrides.
8
+ *
9
+ * Exports dual hooks: `useApi()` (throws if no provider) and `useApiSafe()` (returns null).
10
+ */
1
11
  import React from 'react';
2
12
  export interface NetworkClient {
3
13
  request: <T>(url: string, options?: RequestInit) => Promise<T>;
@@ -1,4 +1,14 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * @fileoverview API context and provider for React Native (Firebase entry only).
4
+ *
5
+ * Provides a network client context with typed HTTP methods (get, post, put, delete),
6
+ * authentication state (token, userId, isReady, isLoading), and optional token
7
+ * refresh. Uses a default fetch-based client with JSON content type, with support
8
+ * for custom network client overrides.
9
+ *
10
+ * Exports dual hooks: `useApi()` (throws if no provider) and `useApiSafe()` (returns null).
11
+ */
2
12
  import { createContext, useContext, useMemo } from 'react';
3
13
  const ApiContext = createContext(null);
4
14
  function createDefaultNetworkClient() {
@@ -1,5 +1,6 @@
1
1
  import type { ComponentType, ReactNode } from 'react';
2
2
  import type { i18n as I18nInstance } from 'i18next';
3
+ import type { ErrorBoundaryProps } from '../components/error/ErrorBoundary';
3
4
  import { Theme } from '../types';
4
5
  /**
5
6
  * Props for the SudobilityAppRN base app wrapper.
@@ -28,16 +29,21 @@ export interface SudobilityAppRNProps {
28
29
  initialTheme?: Theme;
29
30
  /** Storage key prefix for persisted settings */
30
31
  storageKeyPrefix?: string;
32
+ /** Custom error boundary fallback component */
33
+ ErrorFallbackComponent?: ErrorBoundaryProps['FallbackComponent'];
34
+ /** Called when the error boundary catches an error */
35
+ onError?: ErrorBoundaryProps['onError'];
31
36
  }
32
37
  /**
33
38
  * Base app wrapper that composes providers in a specific order (outermost to innermost):
34
39
  *
35
- * 1. SafeAreaProvider (always present)
36
- * 2. I18nextProvider (if `i18n` provided)
37
- * 3. ThemeProvider (or custom `ThemeProviderComponent`)
38
- * 4. QueryClientProvider (if provided)
39
- * 5. ToastProvider (or custom `ToastProviderComponent`)
40
- * 6. AppProviders (custom additional providers)
40
+ * 1. ErrorBoundary (outermost, catches provider initialization errors)
41
+ * 2. SafeAreaProvider (always present)
42
+ * 3. I18nextProvider (if `i18n` provided)
43
+ * 4. ThemeProvider (or custom `ThemeProviderComponent`)
44
+ * 5. QueryClientProvider (if provided)
45
+ * 6. ToastProvider (or custom `ToastProviderComponent`)
46
+ * 7. AppProviders (custom additional providers, innermost)
41
47
  *
42
48
  * @example
43
49
  * ```tsx
@@ -54,4 +60,4 @@ export interface SudobilityAppRNProps {
54
60
  * }
55
61
  * ```
56
62
  */
57
- export declare function SudobilityAppRN({ children, i18n, ThemeProviderComponent, ToastProviderComponent, QueryClientProvider, AppProviders, initialTheme, storageKeyPrefix, }: SudobilityAppRNProps): import("react/jsx-runtime").JSX.Element;
63
+ export declare function SudobilityAppRN({ children, i18n, ThemeProviderComponent, ToastProviderComponent, QueryClientProvider, AppProviders, initialTheme, storageKeyPrefix, ErrorFallbackComponent, onError, }: SudobilityAppRNProps): import("react/jsx-runtime").JSX.Element;
@@ -3,15 +3,17 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';
3
3
  import { I18nextProvider } from 'react-i18next';
4
4
  import { ThemeProvider } from '../theme/ThemeContext';
5
5
  import { ToastProvider } from '../components/toast/ToastProvider';
6
+ import { ErrorBoundary } from '../components/error/ErrorBoundary';
6
7
  /**
7
8
  * Base app wrapper that composes providers in a specific order (outermost to innermost):
8
9
  *
9
- * 1. SafeAreaProvider (always present)
10
- * 2. I18nextProvider (if `i18n` provided)
11
- * 3. ThemeProvider (or custom `ThemeProviderComponent`)
12
- * 4. QueryClientProvider (if provided)
13
- * 5. ToastProvider (or custom `ToastProviderComponent`)
14
- * 6. AppProviders (custom additional providers)
10
+ * 1. ErrorBoundary (outermost, catches provider initialization errors)
11
+ * 2. SafeAreaProvider (always present)
12
+ * 3. I18nextProvider (if `i18n` provided)
13
+ * 4. ThemeProvider (or custom `ThemeProviderComponent`)
14
+ * 5. QueryClientProvider (if provided)
15
+ * 6. ToastProvider (or custom `ToastProviderComponent`)
16
+ * 7. AppProviders (custom additional providers, innermost)
15
17
  *
16
18
  * @example
17
19
  * ```tsx
@@ -28,7 +30,7 @@ import { ToastProvider } from '../components/toast/ToastProvider';
28
30
  * }
29
31
  * ```
30
32
  */
31
- export function SudobilityAppRN({ children, i18n, ThemeProviderComponent, ToastProviderComponent, QueryClientProvider, AppProviders, initialTheme, storageKeyPrefix, }) {
33
+ export function SudobilityAppRN({ children, i18n, ThemeProviderComponent, ToastProviderComponent, QueryClientProvider, AppProviders, initialTheme, storageKeyPrefix, ErrorFallbackComponent, onError, }) {
32
34
  let content = _jsx(_Fragment, { children: children });
33
35
  // Wrap with additional providers (innermost)
34
36
  if (AppProviders) {
@@ -58,5 +60,6 @@ export function SudobilityAppRN({ children, i18n, ThemeProviderComponent, ToastP
58
60
  }
59
61
  // SafeArea (outermost)
60
62
  content = _jsx(SafeAreaProvider, { children: content });
61
- return content;
63
+ // ErrorBoundary (very outermost)
64
+ return (_jsx(ErrorBoundary, { FallbackComponent: ErrorFallbackComponent, onError: onError, children: content }));
62
65
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @fileoverview Reusable error boundary component for React Native.
3
+ *
4
+ * Catches JavaScript errors in its child component tree, logs them,
5
+ * and displays a user-friendly fallback screen with an error message
6
+ * and a retry button. Must be a class component since React does not
7
+ * support error boundary hooks.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * <ErrorBoundary fallbackMessage="Something went wrong.">
12
+ * <MyComponent />
13
+ * </ErrorBoundary>
14
+ * ```
15
+ */
16
+ import React from 'react';
17
+ export interface ErrorBoundaryProps {
18
+ children: React.ReactNode;
19
+ /** Custom fallback component. Receives error and reset function. */
20
+ FallbackComponent?: React.ComponentType<{
21
+ error: Error;
22
+ resetError: () => void;
23
+ }>;
24
+ /** Fallback message displayed in the default fallback UI. */
25
+ fallbackMessage?: string;
26
+ /** Called when an error is caught. Useful for logging. */
27
+ onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
28
+ }
29
+ interface ErrorBoundaryState {
30
+ hasError: boolean;
31
+ error: Error | null;
32
+ }
33
+ export declare class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
34
+ constructor(props: ErrorBoundaryProps);
35
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState;
36
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void;
37
+ resetError: () => void;
38
+ render(): React.ReactNode;
39
+ }
40
+ export {};
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * @fileoverview Reusable error boundary component for React Native.
4
+ *
5
+ * Catches JavaScript errors in its child component tree, logs them,
6
+ * and displays a user-friendly fallback screen with an error message
7
+ * and a retry button. Must be a class component since React does not
8
+ * support error boundary hooks.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * <ErrorBoundary fallbackMessage="Something went wrong.">
13
+ * <MyComponent />
14
+ * </ErrorBoundary>
15
+ * ```
16
+ */
17
+ import React from 'react';
18
+ import { View, Text, Pressable, StyleSheet } from 'react-native';
19
+ export class ErrorBoundary extends React.Component {
20
+ constructor(props) {
21
+ super(props);
22
+ this.resetError = () => {
23
+ this.setState({ hasError: false, error: null });
24
+ };
25
+ this.state = { hasError: false, error: null };
26
+ }
27
+ static getDerivedStateFromError(error) {
28
+ return { hasError: true, error };
29
+ }
30
+ componentDidCatch(error, errorInfo) {
31
+ this.props.onError?.(error, errorInfo);
32
+ }
33
+ render() {
34
+ if (this.state.hasError && this.state.error) {
35
+ if (this.props.FallbackComponent) {
36
+ return (_jsx(this.props.FallbackComponent, { error: this.state.error, resetError: this.resetError }));
37
+ }
38
+ return (_jsxs(View, { style: styles.container, accessibilityRole: 'summary', accessibilityLabel: 'An error has occurred', children: [_jsx(Text, { style: styles.title, children: "Oops!" }), _jsx(Text, { style: styles.message, children: this.props.fallbackMessage ??
39
+ 'Something went wrong. Please try again.' }), _jsx(Text, { style: styles.errorDetail, children: this.state.error.message }), _jsx(Pressable, { style: styles.retryButton, onPress: this.resetError, accessibilityRole: 'button', accessibilityLabel: 'Try again', children: _jsx(Text, { style: styles.retryButtonText, children: "Try Again" }) })] }));
40
+ }
41
+ return this.props.children;
42
+ }
43
+ }
44
+ const styles = StyleSheet.create({
45
+ container: {
46
+ flex: 1,
47
+ justifyContent: 'center',
48
+ alignItems: 'center',
49
+ padding: 24,
50
+ backgroundColor: '#f9fafb',
51
+ },
52
+ title: {
53
+ fontSize: 24,
54
+ fontWeight: '700',
55
+ color: '#111827',
56
+ marginBottom: 8,
57
+ },
58
+ message: {
59
+ fontSize: 16,
60
+ color: '#4b5563',
61
+ textAlign: 'center',
62
+ marginBottom: 12,
63
+ },
64
+ errorDetail: {
65
+ fontSize: 13,
66
+ color: '#9ca3af',
67
+ textAlign: 'center',
68
+ marginBottom: 24,
69
+ paddingHorizontal: 16,
70
+ },
71
+ retryButton: {
72
+ backgroundColor: '#2563eb',
73
+ borderRadius: 8,
74
+ paddingVertical: 12,
75
+ paddingHorizontal: 24,
76
+ minHeight: 44,
77
+ justifyContent: 'center',
78
+ alignItems: 'center',
79
+ },
80
+ retryButtonText: {
81
+ color: '#ffffff',
82
+ fontSize: 16,
83
+ fontWeight: '600',
84
+ },
85
+ });
@@ -0,0 +1,2 @@
1
+ export { ErrorBoundary } from './ErrorBoundary';
2
+ export type { ErrorBoundaryProps } from './ErrorBoundary';
@@ -0,0 +1 @@
1
+ export { ErrorBoundary } from './ErrorBoundary';
@@ -1,4 +1,13 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * @fileoverview App footer component for React Native.
4
+ *
5
+ * Displays a copyright line with company name/link, version string, and
6
+ * configurable rights text. Below the copyright, it renders a row of
7
+ * footer links (e.g., Privacy, Terms) with dot separators. Links can use
8
+ * `url` (Linking.openURL), `routeName`, or `onPress`. Analytics tracking
9
+ * for link clicks is supported via the `onTrack` prop.
10
+ */
2
11
  import React from 'react';
3
12
  import { View, Text, Pressable, Linking } from 'react-native';
4
13
  import { createThemedStyles } from '../../utils/styles';
@@ -23,7 +32,7 @@ export function AppFooter({ version, copyrightYear, companyName, companyUrl, rig
23
32
  Linking.openURL(companyUrl);
24
33
  }
25
34
  };
26
- return (_jsxs(View, { style: [styles.container, style], children: [_jsxs(View, { style: styles.topRow, children: [version && _jsxs(Text, { style: styles.versionText, children: ["v", version] }), _jsxs(Text, { style: styles.copyrightText, children: [copyrightYear ? `\u00A9 ${copyrightYear} ` : '', companyUrl ? (_jsx(Text, { style: styles.companyLink, onPress: handleCompanyPress, children: companyName })) : (companyName), '. ', rightsText] })] }), links && links.length > 0 && (_jsx(View, { style: styles.linksRow, children: links.map((link, index) => (_jsxs(React.Fragment, { children: [index > 0 && _jsx(Text, { style: styles.separator, children: '\u00B7' }), _jsx(Pressable, { onPress: () => handleLinkPress(link), children: _jsx(Text, { style: styles.linkText, children: link.label }) })] }, link.label))) }))] }));
35
+ return (_jsxs(View, { style: [styles.container, style], accessibilityLabel: 'App footer', children: [_jsxs(View, { style: styles.topRow, children: [version && _jsxs(Text, { style: styles.versionText, children: ["v", version] }), _jsxs(Text, { style: styles.copyrightText, children: [copyrightYear ? `\u00A9 ${copyrightYear} ` : '', companyUrl ? (_jsx(Text, { style: styles.companyLink, onPress: handleCompanyPress, accessibilityRole: 'link', accessibilityLabel: `${companyName} website`, children: companyName })) : (companyName), '. ', rightsText] })] }), links && links.length > 0 && (_jsx(View, { style: styles.linksRow, accessibilityRole: 'list', children: links.map((link, index) => (_jsxs(React.Fragment, { children: [index > 0 && _jsx(Text, { style: styles.separator, children: '\u00B7' }), _jsx(Pressable, { onPress: () => handleLinkPress(link), accessibilityRole: 'link', accessibilityLabel: link.label, children: _jsx(Text, { style: styles.linkText, children: link.label }) })] }, link.label))) }))] }));
27
36
  }
28
37
  const useStyles = createThemedStyles(colors => ({
29
38
  container: {
@@ -1,3 +1,11 @@
1
+ /**
2
+ * @fileoverview App header bar component for React Native.
3
+ *
4
+ * Renders a row with a left section (logo + app name) and a right section
5
+ * (icon buttons from `menuItems`). Both sections can be overridden via
6
+ * `renderLeft` and `renderRight` props. Also provides a
7
+ * `createAppHeaderOptions()` helper for React Navigation integration.
8
+ */
1
9
  import React from 'react';
2
10
  import type { StyleProp, ViewStyle } from 'react-native';
3
11
  import type { MenuItemConfig, LogoConfig } from '../../types';
@@ -7,11 +7,11 @@ export function AppHeader({ logo, menuItems, renderLeft, renderRight, style, })
7
7
  styles.container,
8
8
  { backgroundColor: colors.card, borderBottomColor: colors.border },
9
9
  style,
10
- ], children: [_jsx(View, { style: styles.leftSection, children: renderLeft ? (renderLeft()) : (_jsxs(Pressable, { onPress: logo.onPress, style: styles.logoContainer, children: [logo.source && (_jsx(Image, { source: logo.source, style: styles.logoImage, resizeMode: 'contain' })), _jsx(Text, { style: [styles.appName, { color: colors.text }], children: logo.appName })] })) }), _jsxs(View, { style: styles.rightSection, children: [menuItems
10
+ ], accessibilityRole: 'header', accessibilityLabel: `${logo.appName} header`, children: [_jsx(View, { style: styles.leftSection, children: renderLeft ? (renderLeft()) : (_jsxs(Pressable, { onPress: logo.onPress, style: styles.logoContainer, accessibilityRole: 'button', accessibilityLabel: `${logo.appName} home`, children: [logo.source && (_jsx(Image, { source: logo.source, style: styles.logoImage, resizeMode: 'contain', accessibilityLabel: `${logo.appName} logo` })), _jsx(Text, { style: [styles.appName, { color: colors.text }], children: logo.appName })] })) }), _jsxs(View, { style: styles.rightSection, children: [menuItems
11
11
  ?.filter(item => item.show !== false)
12
12
  .map(item => {
13
13
  const IconComponent = item.icon;
14
- return (_jsx(Pressable, { onPress: item.onPress, style: styles.menuButton, accessibilityLabel: item.label, children: _jsx(IconComponent, { size: 22, color: colors.textSecondary }) }, item.id));
14
+ return (_jsx(Pressable, { onPress: item.onPress, style: styles.menuButton, accessibilityRole: 'button', accessibilityLabel: item.label, children: _jsx(IconComponent, { size: 22, color: colors.textSecondary }) }, item.id));
15
15
  }), renderRight?.()] })] }));
16
16
  }
17
17
  /**
@@ -74,6 +74,6 @@ const styles = StyleSheet.create({
74
74
  flexDirection: 'row',
75
75
  alignItems: 'center',
76
76
  gap: 4,
77
- marginRight: 8,
77
+ marginEnd: 8,
78
78
  },
79
79
  });
@@ -1,3 +1,10 @@
1
+ /**
2
+ * @fileoverview Screen layout wrapper component for React Native.
3
+ *
4
+ * Provides a SafeAreaView (top, left, right edges) with optional header/footer
5
+ * slots, scrollable (default) or fixed content mode, configurable content padding,
6
+ * and background variants (theme default or card white).
7
+ */
1
8
  import React from 'react';
2
9
  import type { StyleProp, ViewStyle } from 'react-native';
3
10
  export interface AppScreenLayoutProps {
@@ -1,4 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * @fileoverview Full subscription management page for React Native.
4
+ *
5
+ * Displays the current subscription status (active/inactive badge, plan name,
6
+ * expiration date, auto-renew status), followed by package cards with features,
7
+ * pricing, "Most Popular" badge, and purchase buttons. Also includes a
8
+ * "Restore Purchases" button with loading states.
9
+ */
2
10
  import { useState, useCallback } from 'react';
3
11
  import { View, Text, Pressable, ScrollView, ActivityIndicator, } from 'react-native';
4
12
  import { createThemedStyles } from '../../utils/styles';
@@ -52,7 +60,7 @@ export function AppSubscriptionPage({ currentStatus, packages, labels = {}, form
52
60
  setLoading(null);
53
61
  }
54
62
  }, [loading, onRestore, onRestoreSuccess, onError, onTrack]);
55
- return (_jsxs(ScrollView, { style: [styles.container, style], contentContainerStyle: styles.content, children: [currentStatus && (_jsxs(View, { style: styles.statusCard, children: [_jsx(View, { style: styles.statusHeader, children: _jsx(View, { style: [
63
+ return (_jsxs(ScrollView, { style: [styles.container, style], contentContainerStyle: styles.content, children: [currentStatus && (_jsxs(View, { style: styles.statusCard, accessibilityRole: 'summary', accessibilityLabel: `Subscription status: ${currentStatus.isActive ? 'Active' : 'Inactive'}`, children: [_jsx(View, { style: styles.statusHeader, children: _jsx(View, { style: [
56
64
  styles.statusBadge,
57
65
  currentStatus.isActive
58
66
  ? styles.statusBadgeActive
@@ -67,16 +75,22 @@ export function AppSubscriptionPage({ currentStatus, packages, labels = {}, form
67
75
  : (labels.statusInactive ?? 'Inactive') }) }) }), currentStatus.isActive ? (_jsxs(View, { style: styles.statusFields, children: [currentStatus.planName && (_jsxs(View, { style: styles.statusField, children: [_jsx(Text, { style: styles.statusFieldLabel, children: labels.labelPlan ?? 'Plan' }), _jsx(Text, { style: styles.statusFieldValue, children: currentStatus.planName })] })), currentStatus.expirationDate && (_jsxs(View, { style: styles.statusField, children: [_jsx(Text, { style: styles.statusFieldLabel, children: labels.labelExpires ?? 'Expires' }), _jsx(Text, { style: styles.statusFieldValue, children: currentStatus.expirationDate })] })), currentStatus.willRenew !== undefined && (_jsxs(View, { style: styles.statusField, children: [_jsx(Text, { style: styles.statusFieldLabel, children: labels.labelRenews ?? 'Auto-renews' }), _jsx(Text, { style: styles.statusFieldValue, children: currentStatus.willRenew
68
76
  ? (labels.yes ?? 'Yes')
69
77
  : (labels.no ?? 'No') })] }))] })) : (_jsx(Text, { style: styles.statusInactiveMessage, children: labels.statusInactiveMessage ??
70
- 'Subscribe to unlock premium features' }))] })), externalLoading && (_jsx(View, { style: styles.loadingContainer, children: _jsx(ActivityIndicator, { size: 'large', color: styles.loadingIndicator.color }) })), !externalLoading && packages.length > 0 && (_jsx(View, { style: styles.packageList, children: packages.map(pkg => (_jsxs(View, { style: [
78
+ 'Subscribe to unlock premium features' }))] })), externalLoading && (_jsx(View, { style: styles.loadingContainer, children: _jsx(ActivityIndicator, { size: 'large', color: styles.loadingIndicator.color, accessibilityLabel: 'Loading subscription packages' }) })), !externalLoading && packages.length > 0 && (_jsx(View, { style: styles.packageList, accessibilityRole: 'list', children: packages.map(pkg => (_jsxs(View, { style: [
71
79
  styles.packageCard,
72
80
  pkg.isMostPopular && styles.packageCardPopular,
73
- ], 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: [
81
+ ], 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: [
74
82
  styles.purchaseButton,
75
83
  loading === pkg.id && styles.purchaseButtonDisabled,
76
- ], 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: [
84
+ ], onPress: () => handlePurchase(pkg), disabled: loading !== null, accessibilityRole: 'button', accessibilityLabel: `${labels.purchase ?? 'Subscribe'} to ${pkg.title}`, accessibilityState: {
85
+ disabled: loading !== null,
86
+ busy: loading === pkg.id,
87
+ }, children: loading === pkg.id ? (_jsx(ActivityIndicator, { color: '#ffffff', size: 'small' })) : (_jsx(Text, { style: styles.purchaseButtonText, children: labels.purchase ?? 'Subscribe' })) }))] }, pkg.id))) })), _jsx(Pressable, { style: [
77
88
  styles.restoreButton,
78
89
  loading === 'restore' && styles.restoreButtonDisabled,
79
- ], 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 }))] }));
90
+ ], onPress: handleRestore, disabled: loading !== null, accessibilityRole: 'button', accessibilityLabel: labels.restore ?? 'Restore Purchases', accessibilityState: {
91
+ disabled: loading !== null,
92
+ busy: loading === 'restore',
93
+ }, 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 }))] }));
80
94
  }
81
95
  const useStyles = createThemedStyles(colors => ({
82
96
  container: {
@@ -1,3 +1,11 @@
1
+ /**
2
+ * @fileoverview Structured text content screen for React Native.
3
+ *
4
+ * Renders structured `TextPageContent` (title, sections with subsections,
5
+ * bullet lists, and contact block). Suitable for privacy policy, terms of
6
+ * service, and similar static content pages. Supports optional `ScreenWrapper`
7
+ * prop for custom layout wrapping.
8
+ */
1
9
  import React from 'react';
2
10
  import type { StyleProp, ViewStyle } from 'react-native';
3
11
  import type { TextPageContent } from '../../types';
@@ -62,12 +62,12 @@ const useStyles = createThemedStyles(colors => ({
62
62
  },
63
63
  listItem: {
64
64
  flexDirection: 'row',
65
- paddingLeft: 8,
65
+ paddingStart: 8,
66
66
  },
67
67
  bullet: {
68
68
  fontSize: 15,
69
69
  color: colors.textSecondary,
70
- marginRight: 8,
70
+ marginEnd: 8,
71
71
  lineHeight: 22,
72
72
  },
73
73
  listItemText: {
@@ -1,4 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * @fileoverview Login screen component for React Native.
4
+ *
5
+ * Provides an email/password form with sign-in and sign-up toggle, optional
6
+ * Google and Apple OAuth buttons, loading states with ActivityIndicator,
7
+ * and inline error display. Uses KeyboardAvoidingView for proper keyboard handling.
8
+ *
9
+ * @platform ios - Uses `behavior="padding"` for KeyboardAvoidingView, which
10
+ * adjusts the bottom padding to keep form fields visible above the keyboard.
11
+ * @platform android - Uses `behavior="height"` for KeyboardAvoidingView, which
12
+ * adjusts the component height. This is more reliable on Android where padding
13
+ * behavior can conflict with system window insets.
14
+ */
2
15
  import { useState, useCallback } from 'react';
3
16
  import { View, Text, TextInput, Pressable, ActivityIndicator, Image, KeyboardAvoidingView, Platform, } from 'react-native';
4
17
  import { createThemedStyles } from '../../utils/styles';
@@ -44,13 +57,15 @@ export function LoginScreen({ appName, logo, onLogin, onSignUp, onGoogleSignIn,
44
57
  setLoading(false);
45
58
  }
46
59
  }, []);
47
- return (_jsx(KeyboardAvoidingView, { style: [styles.container, style], behavior: Platform.OS === 'ios' ? 'padding' : 'height', children: _jsxs(View, { style: styles.card, children: [logo && (_jsx(Image, { source: logo, style: styles.logo, resizeMode: 'contain' })), _jsx(Text, { style: styles.title, children: isSignUp ? `Sign up for ${appName}` : `Sign in to ${appName}` }), error && (_jsx(View, { style: styles.errorBox, children: _jsx(Text, { style: styles.errorText, children: error }) })), _jsxs(View, { style: styles.form, children: [_jsx(Text, { style: styles.label, children: "Email" }), _jsx(TextInput, { style: styles.input, value: email, onChangeText: setEmail, placeholder: 'you@example.com', autoCapitalize: 'none', autoCorrect: false, keyboardType: 'email-address', textContentType: 'emailAddress', editable: !loading }), _jsx(Text, { style: styles.label, children: "Password" }), _jsx(TextInput, { style: styles.input, value: password, onChangeText: setPassword, placeholder: 'Password', secureTextEntry: true, textContentType: isSignUp ? 'newPassword' : 'password', editable: !loading, onSubmitEditing: handleSubmit }), _jsx(Pressable, { style: [
60
+ return (_jsx(KeyboardAvoidingView, { style: [styles.container, style], behavior: Platform.OS === 'ios' ? 'padding' : 'height', children: _jsxs(View, { style: styles.card, accessibilityLabel: 'Login form', children: [logo && (_jsx(Image, { source: logo, style: styles.logo, resizeMode: 'contain', accessibilityLabel: `${appName} logo` })), _jsx(Text, { style: styles.title, accessibilityRole: 'header', accessibilityLabel: isSignUp ? `Sign up for ${appName}` : `Sign in to ${appName}`, children: isSignUp ? `Sign up for ${appName}` : `Sign in to ${appName}` }), error && (_jsx(View, { style: styles.errorBox, accessibilityRole: 'alert', accessibilityLabel: `Error: ${error}`, children: _jsx(Text, { style: styles.errorText, children: error }) })), _jsxs(View, { style: styles.form, children: [_jsx(Text, { style: styles.label, accessibilityLabel: 'Email address', children: "Email" }), _jsx(TextInput, { style: styles.input, value: email, onChangeText: setEmail, placeholder: 'you@example.com', autoCapitalize: 'none', autoCorrect: false, keyboardType: 'email-address', textContentType: 'emailAddress', editable: !loading, accessibilityLabel: 'Email address input', accessibilityHint: 'Enter your email address' }), _jsx(Text, { style: styles.label, accessibilityLabel: 'Password', children: "Password" }), _jsx(TextInput, { style: styles.input, value: password, onChangeText: setPassword, placeholder: 'Password', secureTextEntry: true, textContentType: isSignUp ? 'newPassword' : 'password', editable: !loading, onSubmitEditing: handleSubmit, accessibilityLabel: 'Password input', accessibilityHint: 'Enter your password' }), _jsx(Pressable, { style: [
48
61
  styles.submitButton,
49
62
  loading && styles.submitButtonDisabled,
50
- ], onPress: handleSubmit, disabled: loading, children: loading ? (_jsx(ActivityIndicator, { color: '#ffffff', size: 'small' })) : (_jsx(Text, { style: styles.submitButtonText, children: isSignUp ? 'Sign Up' : 'Sign In' })) })] }), (showGoogleSignIn || showAppleSignIn) && (_jsxs(View, { style: styles.socialSection, children: [_jsxs(View, { style: styles.divider, children: [_jsx(View, { style: styles.dividerLine }), _jsx(Text, { style: styles.dividerText, children: "or" }), _jsx(View, { style: styles.dividerLine })] }), showGoogleSignIn && onGoogleSignIn && (_jsx(Pressable, { style: styles.socialButton, onPress: () => handleSocialSignIn(onGoogleSignIn), disabled: loading, children: _jsx(Text, { style: styles.socialButtonText, children: "Continue with Google" }) })), showAppleSignIn && onAppleSignIn && (_jsx(Pressable, { style: [styles.socialButton, styles.appleButton], onPress: () => handleSocialSignIn(onAppleSignIn), disabled: loading, children: _jsx(Text, { style: [styles.socialButtonText, styles.appleButtonText], children: "Continue with Apple" }) }))] })), showSignUp && onSignUp && (_jsx(Pressable, { style: styles.toggleLink, onPress: () => {
63
+ ], onPress: handleSubmit, disabled: loading, accessibilityRole: 'button', accessibilityLabel: isSignUp ? 'Sign Up' : 'Sign In', accessibilityState: { disabled: loading, busy: loading }, children: loading ? (_jsx(ActivityIndicator, { color: '#ffffff', size: 'small' })) : (_jsx(Text, { style: styles.submitButtonText, children: isSignUp ? 'Sign Up' : 'Sign In' })) })] }), (showGoogleSignIn || showAppleSignIn) && (_jsxs(View, { style: styles.socialSection, children: [_jsxs(View, { style: styles.divider, children: [_jsx(View, { style: styles.dividerLine }), _jsx(Text, { style: styles.dividerText, children: "or" }), _jsx(View, { style: styles.dividerLine })] }), showGoogleSignIn && onGoogleSignIn && (_jsx(Pressable, { style: styles.socialButton, onPress: () => handleSocialSignIn(onGoogleSignIn), disabled: loading, accessibilityRole: 'button', accessibilityLabel: 'Continue with Google', accessibilityState: { disabled: loading }, children: _jsx(Text, { style: styles.socialButtonText, children: "Continue with Google" }) })), showAppleSignIn && onAppleSignIn && (_jsx(Pressable, { style: [styles.socialButton, styles.appleButton], onPress: () => handleSocialSignIn(onAppleSignIn), disabled: loading, accessibilityRole: 'button', accessibilityLabel: 'Continue with Apple', accessibilityState: { disabled: loading }, children: _jsx(Text, { style: [styles.socialButtonText, styles.appleButtonText], children: "Continue with Apple" }) }))] })), showSignUp && onSignUp && (_jsx(Pressable, { style: styles.toggleLink, onPress: () => {
51
64
  setIsSignUp(!isSignUp);
52
65
  setError(null);
53
- }, children: _jsx(Text, { style: styles.toggleText, children: isSignUp
66
+ }, accessibilityRole: 'button', accessibilityLabel: isSignUp
67
+ ? 'Already have an account? Sign in'
68
+ : "Don't have an account? Sign up", children: _jsx(Text, { style: styles.toggleText, children: isSignUp
54
69
  ? 'Already have an account? Sign in'
55
70
  : "Don't have an account? Sign up" }) }))] }) }));
56
71
  }
@@ -3,7 +3,11 @@ import { View, Text, Pressable } from 'react-native';
3
3
  import { Theme, FontSize } from '../../types';
4
4
  import { createThemedStyles } from '../../utils/styles';
5
5
  const THEME_OPTIONS = [
6
- { value: Theme.LIGHT, labelKey: 'appearance.theme.light', fallback: 'Light' },
6
+ {
7
+ value: Theme.LIGHT,
8
+ labelKey: 'appearance.theme.light',
9
+ fallback: 'Light',
10
+ },
7
11
  { value: Theme.DARK, labelKey: 'appearance.theme.dark', fallback: 'Dark' },
8
12
  {
9
13
  value: Theme.SYSTEM,
@@ -31,19 +35,19 @@ const FONT_SIZE_OPTIONS = [
31
35
  export function AppearanceSettings({ theme, fontSize, onThemeChange, onFontSizeChange, t, style, showInfoBox = false, }) {
32
36
  const styles = useStyles();
33
37
  const translate = (key, fallback) => t?.(key, fallback) ?? fallback;
34
- return (_jsxs(View, { style: [styles.container, style], children: [_jsx(Text, { style: styles.heading, children: translate('appearance.title', 'Appearance') }), _jsx(Text, { style: styles.label, children: translate('appearance.theme.label', 'Theme') }), _jsx(View, { style: styles.segmentedControl, children: THEME_OPTIONS.map(option => (_jsx(Pressable, { style: [
35
- styles.segment,
36
- theme === option.value && styles.segmentActive,
37
- ], onPress: () => onThemeChange(option.value), children: _jsx(Text, { style: [
38
- styles.segmentText,
39
- theme === option.value && styles.segmentTextActive,
40
- ], children: translate(option.labelKey, option.fallback) }) }, option.value))) }), _jsx(Text, { style: [styles.label, { marginTop: 20 }], children: translate('appearance.fontSize.label', 'Font Size') }), _jsx(View, { style: styles.segmentedControl, children: FONT_SIZE_OPTIONS.map(option => (_jsx(Pressable, { style: [
41
- styles.segment,
42
- fontSize === option.value && styles.segmentActive,
43
- ], onPress: () => onFontSizeChange(option.value), children: _jsx(Text, { style: [
44
- styles.segmentText,
45
- fontSize === option.value && styles.segmentTextActive,
46
- ], children: translate(option.labelKey, option.fallback) }) }, option.value))) }), showInfoBox && (_jsx(View, { style: styles.infoBox, children: _jsx(Text, { style: styles.infoText, children: translate('appearance.infoBox', 'Your appearance preferences are stored locally on this device.') }) }))] }));
38
+ return (_jsxs(View, { style: [styles.container, style], children: [_jsx(Text, { style: styles.heading, accessibilityRole: 'header', children: translate('appearance.title', 'Appearance') }), _jsx(Text, { style: styles.label, children: translate('appearance.theme.label', 'Theme') }), _jsx(View, { style: styles.segmentedControl, accessibilityRole: 'radiogroup', accessibilityLabel: translate('appearance.theme.label', 'Theme'), children: THEME_OPTIONS.map(option => {
39
+ const isSelected = theme === option.value;
40
+ return (_jsx(Pressable, { style: [styles.segment, isSelected && styles.segmentActive], onPress: () => onThemeChange(option.value), accessibilityRole: 'radio', accessibilityState: { selected: isSelected }, accessibilityLabel: translate(option.labelKey, option.fallback), children: _jsx(Text, { style: [
41
+ styles.segmentText,
42
+ isSelected && styles.segmentTextActive,
43
+ ], children: translate(option.labelKey, option.fallback) }) }, option.value));
44
+ }) }), _jsx(Text, { style: [styles.label, { marginTop: 20 }], children: translate('appearance.fontSize.label', 'Font Size') }), _jsx(View, { style: styles.segmentedControl, accessibilityRole: 'radiogroup', accessibilityLabel: translate('appearance.fontSize.label', 'Font Size'), children: FONT_SIZE_OPTIONS.map(option => {
45
+ const isSelected = fontSize === option.value;
46
+ return (_jsx(Pressable, { style: [styles.segment, isSelected && styles.segmentActive], onPress: () => onFontSizeChange(option.value), accessibilityRole: 'radio', accessibilityState: { selected: isSelected }, accessibilityLabel: translate(option.labelKey, option.fallback), children: _jsx(Text, { style: [
47
+ styles.segmentText,
48
+ isSelected && styles.segmentTextActive,
49
+ ], children: translate(option.labelKey, option.fallback) }) }, option.value));
50
+ }) }), showInfoBox && (_jsx(View, { style: styles.infoBox, children: _jsx(Text, { style: styles.infoText, children: translate('appearance.infoBox', 'Your appearance preferences are stored locally on this device.') }) }))] }));
47
51
  }
48
52
  const useStyles = createThemedStyles(colors => ({
49
53
  container: {
@@ -1,4 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * @fileoverview Modal-based language selector for React Native.
4
+ *
5
+ * Displays a trigger button showing the current language (flag + name) with
6
+ * a dropdown chevron. Pressing the trigger opens a full-screen modal with a
7
+ * FlatList of all available languages, each showing flag + name + checkmark
8
+ * for the current selection.
9
+ *
10
+ * @platform ios - Uses `presentationStyle="pageSheet"` for the modal, which
11
+ * presents as a card that can be swiped to dismiss (iOS 13+).
12
+ * @platform android - Uses the default modal presentation (full-screen overlay).
13
+ * The `presentationStyle` prop is ignored on Android.
14
+ */
2
15
  import { useState } from 'react';
3
16
  import { View, Text, Pressable, Modal, FlatList, SafeAreaView, } from 'react-native';
4
17
  import { DEFAULT_LANGUAGES } from '../../constants/languages';
@@ -11,12 +24,14 @@ export function LanguagePicker({ languages = DEFAULT_LANGUAGES, currentLanguage
11
24
  onLanguageChange?.(code);
12
25
  setModalVisible(false);
13
26
  };
14
- return (_jsxs(View, { style: style, children: [label && _jsx(Text, { style: styles.label, children: label }), _jsxs(Pressable, { style: styles.trigger, onPress: () => setModalVisible(true), children: [_jsx(Text, { style: styles.triggerText, children: currentLang
27
+ return (_jsxs(View, { style: style, children: [label && _jsx(Text, { style: styles.label, children: label }), _jsxs(Pressable, { style: styles.trigger, onPress: () => setModalVisible(true), accessibilityRole: 'button', accessibilityLabel: `${label ?? 'Language'}: ${currentLang?.name ?? currentLanguage}. Tap to change.`, children: [_jsx(Text, { style: styles.triggerText, children: currentLang
15
28
  ? `${currentLang.flag} ${currentLang.name}`
16
- : currentLanguage }), _jsx(Text, { style: styles.chevron, children: '\u25BC' })] }), _jsx(Modal, { visible: modalVisible, animationType: 'slide', presentationStyle: 'pageSheet', onRequestClose: () => setModalVisible(false), children: _jsxs(SafeAreaView, { style: styles.modal, children: [_jsxs(View, { style: styles.modalHeader, children: [_jsx(Text, { style: styles.modalTitle, children: label ?? 'Select Language' }), _jsx(Pressable, { onPress: () => setModalVisible(false), children: _jsx(Text, { style: styles.closeButton, children: "Done" }) })] }), _jsx(FlatList, { data: languages, keyExtractor: item => item.code, renderItem: ({ item }) => (_jsxs(Pressable, { style: [
29
+ : currentLanguage }), _jsx(Text, { style: styles.chevron, children: '\u25BC' })] }), _jsx(Modal, { visible: modalVisible, animationType: 'slide', presentationStyle: 'pageSheet', onRequestClose: () => setModalVisible(false), children: _jsxs(SafeAreaView, { style: styles.modal, children: [_jsxs(View, { style: styles.modalHeader, children: [_jsx(Text, { style: styles.modalTitle, accessibilityRole: 'header', children: label ?? 'Select Language' }), _jsx(Pressable, { onPress: () => setModalVisible(false), accessibilityRole: 'button', accessibilityLabel: 'Done, close language picker', children: _jsx(Text, { style: styles.closeButton, children: "Done" }) })] }), _jsx(FlatList, { data: languages, keyExtractor: item => item.code, renderItem: ({ item }) => (_jsxs(Pressable, { style: [
17
30
  styles.languageRow,
18
31
  item.code === currentLanguage && styles.languageRowActive,
19
- ], onPress: () => handleSelect(item.code), children: [_jsx(Text, { style: styles.flag, children: item.flag }), _jsx(Text, { style: styles.languageName, children: item.name }), item.code === currentLanguage && (_jsx(Text, { style: styles.checkmark, children: '\u2713' }))] })), ItemSeparatorComponent: () => _jsx(View, { style: styles.separator }) })] }) })] }));
32
+ ], onPress: () => handleSelect(item.code), accessibilityRole: 'radio', accessibilityState: {
33
+ selected: item.code === currentLanguage,
34
+ }, accessibilityLabel: `${item.name}${item.code === currentLanguage ? ', selected' : ''}`, children: [_jsx(Text, { style: styles.flag, children: item.flag }), _jsx(Text, { style: styles.languageName, children: item.name }), item.code === currentLanguage && (_jsx(Text, { style: styles.checkmark, children: '\u2713' }))] })), ItemSeparatorComponent: () => _jsx(View, { style: styles.separator }) })] }) })] }));
20
35
  }
21
36
  const useStyles = createThemedStyles(colors => ({
22
37
  label: {
@@ -80,7 +95,7 @@ const useStyles = createThemedStyles(colors => ({
80
95
  },
81
96
  flag: {
82
97
  fontSize: 20,
83
- marginRight: 12,
98
+ marginEnd: 12,
84
99
  },
85
100
  languageName: {
86
101
  fontSize: 16,
@@ -95,6 +110,6 @@ const useStyles = createThemedStyles(colors => ({
95
110
  separator: {
96
111
  height: 1,
97
112
  backgroundColor: colors.border,
98
- marginLeft: 48,
113
+ marginStart: 48,
99
114
  },
100
115
  }));