@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.
- 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
|
@@ -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.
|
|
36
|
-
* 2.
|
|
37
|
-
* 3.
|
|
38
|
-
* 4.
|
|
39
|
-
* 5.
|
|
40
|
-
* 6.
|
|
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.
|
|
10
|
-
* 2.
|
|
11
|
-
* 3.
|
|
12
|
-
* 4.
|
|
13
|
-
* 5.
|
|
14
|
-
* 6.
|
|
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
|
-
|
|
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 @@
|
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
65
|
+
paddingStart: 8,
|
|
66
66
|
},
|
|
67
67
|
bullet: {
|
|
68
68
|
fontSize: 15,
|
|
69
69
|
color: colors.textSecondary,
|
|
70
|
-
|
|
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
|
-
},
|
|
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
|
-
{
|
|
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 =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
113
|
+
marginStart: 48,
|
|
99
114
|
},
|
|
100
115
|
}));
|