@umituz/react-native-settings 4.20.28 → 4.20.30
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/package.json +1 -1
- package/src/domains/cloud-sync/index.ts +7 -0
- package/src/{presentation → domains/cloud-sync/presentation}/components/CloudSyncSetting.tsx +3 -6
- package/src/domains/dev/index.ts +11 -0
- package/src/{presentation → domains/dev/presentation}/components/DevSettingsSection.tsx +4 -6
- package/src/{presentation → domains/dev/presentation}/components/StorageClearSetting.tsx +7 -14
- package/src/domains/video-tutorials/index.ts +12 -2
- package/src/domains/video-tutorials/presentation/components/VideoTutorialSection.tsx +52 -0
- package/src/domains/video-tutorials/presentation/screens/VideoTutorialsScreen.tsx +93 -88
- package/src/index.ts +7 -12
- package/src/presentation/components/SettingsItemCard.tsx +40 -8
- package/src/presentation/navigation/types.ts +1 -1
- package/src/presentation/screens/SettingsScreen.tsx +1 -1
- package/src/presentation/screens/components/SettingsContent.tsx +1 -1
- package/src/presentation/screens/components/sections/SupportSettingsSection.tsx +3 -3
- package/src/domains/video-tutorials/infrastructure/services/video-tutorial.service.ts +0 -117
- package/src/domains/video-tutorials/presentation/hooks/index.ts +0 -36
- package/src/presentation/components/SettingItem.tsx +0 -145
- package/src/presentation/components/__tests__/SettingItem.test.tsx +0 -189
- package/src/presentation/screens/components/sections/SubscriptionSettingsSection.tsx +0 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-settings",
|
|
3
|
-
"version": "4.20.
|
|
3
|
+
"version": "4.20.30",
|
|
4
4
|
"description": "Complete settings hub for React Native apps - consolidated package with settings, about, legal, appearance, feedback, FAQs, and rating",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
package/src/{presentation → domains/cloud-sync/presentation}/components/CloudSyncSetting.tsx
RENAMED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React, { useMemo } from "react";
|
|
7
|
-
import {
|
|
7
|
+
import { SettingsItemCard } from "../../../../presentation/components/SettingsItemCard";
|
|
8
8
|
|
|
9
9
|
export interface CloudSyncSettingProps {
|
|
10
10
|
title?: string;
|
|
@@ -13,7 +13,6 @@ export interface CloudSyncSettingProps {
|
|
|
13
13
|
lastSynced?: Date | null;
|
|
14
14
|
onPress?: () => void;
|
|
15
15
|
disabled?: boolean;
|
|
16
|
-
isLast?: boolean;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
const formatLastSynced = (date: Date): string => {
|
|
@@ -42,7 +41,6 @@ export const CloudSyncSetting: React.FC<CloudSyncSettingProps> = ({
|
|
|
42
41
|
lastSynced,
|
|
43
42
|
onPress,
|
|
44
43
|
disabled = false,
|
|
45
|
-
isLast = false,
|
|
46
44
|
}) => {
|
|
47
45
|
const displayValue = useMemo(() => {
|
|
48
46
|
if (isSyncing) {
|
|
@@ -58,13 +56,12 @@ export const CloudSyncSetting: React.FC<CloudSyncSettingProps> = ({
|
|
|
58
56
|
}, [isSyncing, description, lastSynced]);
|
|
59
57
|
|
|
60
58
|
return (
|
|
61
|
-
<
|
|
59
|
+
<SettingsItemCard
|
|
62
60
|
icon="cloud"
|
|
63
61
|
title={title}
|
|
64
|
-
|
|
62
|
+
description={displayValue}
|
|
65
63
|
onPress={onPress}
|
|
66
64
|
disabled={disabled || isSyncing}
|
|
67
|
-
isLast={isLast}
|
|
68
65
|
/>
|
|
69
66
|
);
|
|
70
67
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev Domain
|
|
3
|
+
* Development-only settings and utilities
|
|
4
|
+
* Only visible in __DEV__ mode
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { DevSettingsSection } from "./presentation/components/DevSettingsSection";
|
|
8
|
+
export type { DevSettingsProps } from "./presentation/components/DevSettingsSection";
|
|
9
|
+
|
|
10
|
+
export { StorageClearSetting } from "./presentation/components/StorageClearSetting";
|
|
11
|
+
export type { StorageClearSettingProps } from "./presentation/components/StorageClearSetting";
|
|
@@ -11,8 +11,8 @@ import React from "react";
|
|
|
11
11
|
import { Alert } from "react-native";
|
|
12
12
|
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
13
13
|
import { storageRepository } from "@umituz/react-native-storage";
|
|
14
|
-
import { SettingsSection } from "
|
|
15
|
-
import {
|
|
14
|
+
import { SettingsSection } from "../../../../presentation/components/SettingsSection";
|
|
15
|
+
import { SettingsItemCard } from "../../../../presentation/components/SettingsItemCard";
|
|
16
16
|
|
|
17
17
|
// Default texts (English only - DEV feature)
|
|
18
18
|
const DEFAULT_TEXTS = {
|
|
@@ -100,14 +100,12 @@ export const DevSettingsSection: React.FC<DevSettingsProps> = ({
|
|
|
100
100
|
return (
|
|
101
101
|
<SettingsSection title={t.sectionTitle}>
|
|
102
102
|
{customDevComponents.map((component) => component)}
|
|
103
|
-
<
|
|
103
|
+
<SettingsItemCard
|
|
104
104
|
icon="trash-outline"
|
|
105
105
|
title={t.clearTitle}
|
|
106
|
-
|
|
106
|
+
description={t.clearDescription}
|
|
107
107
|
onPress={handleClearData}
|
|
108
108
|
iconColor={tokens.colors.error}
|
|
109
|
-
titleColor={tokens.colors.error}
|
|
110
|
-
isLast={true}
|
|
111
109
|
/>
|
|
112
110
|
</SettingsSection>
|
|
113
111
|
);
|
|
@@ -5,15 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import React from "react";
|
|
8
|
-
import {
|
|
8
|
+
import { SettingsItemCard } from "../../../../presentation/components/SettingsItemCard";
|
|
9
9
|
|
|
10
10
|
export interface StorageClearSettingProps {
|
|
11
11
|
title?: string;
|
|
12
12
|
description?: string;
|
|
13
13
|
onPress?: () => void;
|
|
14
14
|
iconColor?: string;
|
|
15
|
-
titleColor?: string;
|
|
16
|
-
isLast?: boolean;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
export const StorageClearSetting: React.FC<StorageClearSettingProps> = ({
|
|
@@ -21,28 +19,23 @@ export const StorageClearSetting: React.FC<StorageClearSettingProps> = ({
|
|
|
21
19
|
description,
|
|
22
20
|
onPress,
|
|
23
21
|
iconColor,
|
|
24
|
-
titleColor,
|
|
25
|
-
isLast = false,
|
|
26
22
|
}) => {
|
|
27
|
-
// Default values for DEV mode
|
|
28
|
-
const defaultTitle = title || "Clear All Storage";
|
|
29
|
-
const defaultDescription = description || "Clear all local storage data (DEV only)";
|
|
30
|
-
const defaultIconColor = iconColor || "#EF4444";
|
|
31
|
-
const defaultTitleColor = titleColor || "#EF4444";
|
|
32
23
|
// Only render in DEV mode
|
|
33
24
|
if (!__DEV__) {
|
|
34
25
|
return null;
|
|
35
26
|
}
|
|
36
27
|
|
|
28
|
+
const defaultTitle = title || "Clear All Storage";
|
|
29
|
+
const defaultDescription = description || "Clear all local storage data (DEV only)";
|
|
30
|
+
const defaultIconColor = iconColor || "#EF4444";
|
|
31
|
+
|
|
37
32
|
return (
|
|
38
|
-
<
|
|
33
|
+
<SettingsItemCard
|
|
39
34
|
icon="trash-outline"
|
|
40
35
|
title={defaultTitle}
|
|
41
|
-
|
|
36
|
+
description={defaultDescription}
|
|
42
37
|
onPress={onPress}
|
|
43
38
|
iconColor={defaultIconColor}
|
|
44
|
-
titleColor={defaultTitleColor}
|
|
45
|
-
isLast={isLast}
|
|
46
39
|
/>
|
|
47
40
|
);
|
|
48
41
|
};
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Video Tutorials Domain
|
|
3
|
+
*
|
|
4
|
+
* Props-based component - pass tutorials data from app config
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { VideoTutorialsScreen, VideoTutorialCard, VideoTutorial } from '@umituz/react-native-settings';
|
|
8
|
+
*
|
|
9
|
+
* <VideoTutorialsScreen
|
|
10
|
+
* tutorials={myTutorials}
|
|
11
|
+
* featuredTutorials={myFeaturedTutorials}
|
|
12
|
+
* onTutorialPress={(tutorial) => Linking.openURL(tutorial.videoUrl)}
|
|
13
|
+
* />
|
|
3
14
|
*/
|
|
4
15
|
|
|
5
16
|
export * from "./types";
|
|
6
17
|
export * from "./presentation/screens/VideoTutorialsScreen";
|
|
7
18
|
export * from "./presentation/components/VideoTutorialCard";
|
|
8
|
-
export * from "./presentation/
|
|
9
|
-
export * from "./infrastructure/services/video-tutorial.service";
|
|
19
|
+
export * from "./presentation/components/VideoTutorialSection";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Tutorial Section Component
|
|
3
|
+
* Renders Video Tutorial entry point for settings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
export interface VideoTutorialConfig {
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
onPress?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface VideoTutorialSectionProps {
|
|
16
|
+
config: VideoTutorialConfig;
|
|
17
|
+
renderSection: (props: {
|
|
18
|
+
title: string;
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
}) => React.ReactElement | null;
|
|
21
|
+
renderItem: (props: {
|
|
22
|
+
title: string;
|
|
23
|
+
icon: any;
|
|
24
|
+
onPress: () => void;
|
|
25
|
+
isLast?: boolean;
|
|
26
|
+
}) => React.ReactElement | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const VideoTutorialSection: React.FC<VideoTutorialSectionProps> = ({
|
|
30
|
+
config,
|
|
31
|
+
renderSection,
|
|
32
|
+
renderItem,
|
|
33
|
+
}) => {
|
|
34
|
+
// onPress is required for VideoTutorial section to be functional
|
|
35
|
+
if (!config.enabled || !config.title || !config.description || !config.onPress) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
{renderSection({
|
|
42
|
+
title: config.title,
|
|
43
|
+
children: renderItem({
|
|
44
|
+
title: config.description,
|
|
45
|
+
icon: 'play-circle',
|
|
46
|
+
onPress: config.onPress,
|
|
47
|
+
isLast: true,
|
|
48
|
+
}),
|
|
49
|
+
})}
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Video Tutorials Screen
|
|
3
3
|
* Single Responsibility: Display video tutorials list
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <VideoTutorialsScreen
|
|
7
|
+
* tutorials={[...]}
|
|
8
|
+
* featuredTutorials={[...]}
|
|
9
|
+
* title="Video Tutorials"
|
|
10
|
+
* onTutorialPress={(id) => openVideo(id)}
|
|
11
|
+
* />
|
|
4
12
|
*/
|
|
5
13
|
|
|
6
14
|
import React from "react";
|
|
@@ -13,9 +21,16 @@ import {
|
|
|
13
21
|
} from "@umituz/react-native-design-system";
|
|
14
22
|
import type { VideoTutorial } from "../../types";
|
|
15
23
|
import { VideoTutorialCard } from "../components/VideoTutorialCard";
|
|
16
|
-
import { useVideoTutorials, useFeaturedTutorials } from "../hooks";
|
|
17
24
|
|
|
18
25
|
export interface VideoTutorialsScreenProps {
|
|
26
|
+
/**
|
|
27
|
+
* All tutorials to display (required)
|
|
28
|
+
*/
|
|
29
|
+
tutorials: VideoTutorial[];
|
|
30
|
+
/**
|
|
31
|
+
* Featured tutorials to display in horizontal scroll (optional)
|
|
32
|
+
*/
|
|
33
|
+
featuredTutorials?: VideoTutorial[];
|
|
19
34
|
/**
|
|
20
35
|
* Title of the screen
|
|
21
36
|
*/
|
|
@@ -29,63 +44,37 @@ export interface VideoTutorialsScreenProps {
|
|
|
29
44
|
*/
|
|
30
45
|
allTutorialsTitle?: string;
|
|
31
46
|
/**
|
|
32
|
-
*
|
|
47
|
+
* Empty state message when no tutorials
|
|
33
48
|
*/
|
|
34
|
-
|
|
49
|
+
emptyMessage?: string;
|
|
35
50
|
/**
|
|
36
|
-
*
|
|
51
|
+
* Loading state (optional - defaults to false)
|
|
37
52
|
*/
|
|
38
|
-
|
|
53
|
+
isLoading?: boolean;
|
|
39
54
|
/**
|
|
40
55
|
* Callback when a tutorial is pressed
|
|
41
56
|
*/
|
|
42
|
-
onTutorialPress?: (
|
|
43
|
-
/**
|
|
44
|
-
* Optional manual override for loading state
|
|
45
|
-
*/
|
|
46
|
-
customIsLoading?: boolean;
|
|
47
|
-
/**
|
|
48
|
-
* Optional manual override for all tutorials data
|
|
49
|
-
*/
|
|
50
|
-
customAllTutorials?: VideoTutorial[];
|
|
51
|
-
/**
|
|
52
|
-
* Optional manual override for featured tutorials data
|
|
53
|
-
*/
|
|
54
|
-
customFeaturedTutorials?: VideoTutorial[];
|
|
57
|
+
onTutorialPress?: (tutorial: VideoTutorial) => void;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
export const VideoTutorialsScreen: React.FC<VideoTutorialsScreenProps> =
|
|
58
61
|
React.memo(
|
|
59
62
|
({
|
|
63
|
+
tutorials,
|
|
64
|
+
featuredTutorials,
|
|
60
65
|
title = "Video Tutorials",
|
|
61
66
|
featuredTitle = "Featured",
|
|
62
67
|
allTutorialsTitle = "All Tutorials",
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
emptyMessage = "No tutorials available.",
|
|
69
|
+
isLoading = false,
|
|
65
70
|
onTutorialPress,
|
|
66
|
-
customIsLoading,
|
|
67
|
-
customAllTutorials,
|
|
68
|
-
customFeaturedTutorials,
|
|
69
71
|
}) => {
|
|
70
72
|
const tokens = useAppDesignTokens();
|
|
71
73
|
const styles = getStyles(tokens);
|
|
72
74
|
|
|
73
|
-
const featuredQuery = useFeaturedTutorials(maxFeaturedCount);
|
|
74
|
-
const allQuery = useVideoTutorials();
|
|
75
|
-
|
|
76
|
-
const isLoading =
|
|
77
|
-
customIsLoading !== undefined
|
|
78
|
-
? customIsLoading
|
|
79
|
-
: featuredQuery.isLoading || allQuery.isLoading;
|
|
80
|
-
const error = featuredQuery.error || allQuery.error;
|
|
81
|
-
|
|
82
|
-
const featuredTutorials =
|
|
83
|
-
customFeaturedTutorials || featuredQuery.data || [];
|
|
84
|
-
const allTutorials = customAllTutorials || allQuery.data || [];
|
|
85
|
-
|
|
86
75
|
const handleTutorialPress = React.useCallback(
|
|
87
|
-
(
|
|
88
|
-
onTutorialPress?.(
|
|
76
|
+
(tutorial: VideoTutorial) => {
|
|
77
|
+
onTutorialPress?.(tutorial);
|
|
89
78
|
},
|
|
90
79
|
[onTutorialPress],
|
|
91
80
|
);
|
|
@@ -94,7 +83,18 @@ export const VideoTutorialsScreen: React.FC<VideoTutorialsScreenProps> =
|
|
|
94
83
|
({ item }: { item: VideoTutorial }) => (
|
|
95
84
|
<VideoTutorialCard
|
|
96
85
|
tutorial={item}
|
|
97
|
-
onPress={() => handleTutorialPress(item
|
|
86
|
+
onPress={() => handleTutorialPress(item)}
|
|
87
|
+
/>
|
|
88
|
+
),
|
|
89
|
+
[handleTutorialPress],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const renderFeaturedItem = React.useCallback(
|
|
93
|
+
({ item }: { item: VideoTutorial }) => (
|
|
94
|
+
<VideoTutorialCard
|
|
95
|
+
tutorial={item}
|
|
96
|
+
onPress={() => handleTutorialPress(item)}
|
|
97
|
+
horizontal
|
|
98
98
|
/>
|
|
99
99
|
),
|
|
100
100
|
[handleTutorialPress],
|
|
@@ -104,11 +104,14 @@ export const VideoTutorialsScreen: React.FC<VideoTutorialsScreenProps> =
|
|
|
104
104
|
return <AtomicSpinner size="lg" fullContainer />;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
const hasFeatured = featuredTutorials && featuredTutorials.length > 0;
|
|
108
|
+
const hasTutorials = tutorials && tutorials.length > 0;
|
|
109
|
+
|
|
110
|
+
if (!hasTutorials && !hasFeatured) {
|
|
108
111
|
return (
|
|
109
|
-
<View style={styles.
|
|
110
|
-
<AtomicText color="
|
|
111
|
-
{
|
|
112
|
+
<View style={styles.emptyContainer}>
|
|
113
|
+
<AtomicText color="secondary" type="bodyLarge">
|
|
114
|
+
{emptyMessage}
|
|
112
115
|
</AtomicText>
|
|
113
116
|
</View>
|
|
114
117
|
);
|
|
@@ -120,7 +123,7 @@ export const VideoTutorialsScreen: React.FC<VideoTutorialsScreenProps> =
|
|
|
120
123
|
{title}
|
|
121
124
|
</Text>
|
|
122
125
|
|
|
123
|
-
{
|
|
126
|
+
{hasFeatured && (
|
|
124
127
|
<View style={styles.section}>
|
|
125
128
|
<Text
|
|
126
129
|
style={[
|
|
@@ -132,7 +135,7 @@ export const VideoTutorialsScreen: React.FC<VideoTutorialsScreenProps> =
|
|
|
132
135
|
</Text>
|
|
133
136
|
<FlatList
|
|
134
137
|
data={featuredTutorials}
|
|
135
|
-
renderItem={
|
|
138
|
+
renderItem={renderFeaturedItem}
|
|
136
139
|
keyExtractor={(item: VideoTutorial) => item.id}
|
|
137
140
|
horizontal
|
|
138
141
|
showsHorizontalScrollIndicator={false}
|
|
@@ -141,23 +144,25 @@ export const VideoTutorialsScreen: React.FC<VideoTutorialsScreenProps> =
|
|
|
141
144
|
</View>
|
|
142
145
|
)}
|
|
143
146
|
|
|
144
|
-
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
147
|
+
{hasTutorials && (
|
|
148
|
+
<View style={styles.section}>
|
|
149
|
+
<Text
|
|
150
|
+
style={[
|
|
151
|
+
styles.sectionTitle,
|
|
152
|
+
{ color: tokens.colors.textSecondary },
|
|
153
|
+
]}
|
|
154
|
+
>
|
|
155
|
+
{allTutorialsTitle}
|
|
156
|
+
</Text>
|
|
157
|
+
<FlatList
|
|
158
|
+
data={tutorials}
|
|
159
|
+
renderItem={renderTutorialItem}
|
|
160
|
+
keyExtractor={(item: VideoTutorial) => item.id}
|
|
161
|
+
showsVerticalScrollIndicator={false}
|
|
162
|
+
contentContainerStyle={styles.verticalList}
|
|
163
|
+
/>
|
|
164
|
+
</View>
|
|
165
|
+
)}
|
|
161
166
|
</ScreenLayout>
|
|
162
167
|
);
|
|
163
168
|
},
|
|
@@ -165,29 +170,29 @@ export const VideoTutorialsScreen: React.FC<VideoTutorialsScreenProps> =
|
|
|
165
170
|
|
|
166
171
|
const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
|
|
167
172
|
StyleSheet.create({
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
});
|
|
173
|
+
title: {
|
|
174
|
+
fontSize: tokens.typography.headlineLarge.fontSize,
|
|
175
|
+
fontWeight: "600",
|
|
176
|
+
marginBottom: 24,
|
|
177
|
+
},
|
|
178
|
+
section: {
|
|
179
|
+
marginBottom: 24,
|
|
180
|
+
},
|
|
181
|
+
sectionTitle: {
|
|
182
|
+
fontSize: tokens.typography.titleLarge.fontSize,
|
|
183
|
+
fontWeight: "500",
|
|
184
|
+
marginBottom: 12,
|
|
185
|
+
},
|
|
186
|
+
horizontalList: {
|
|
187
|
+
paddingRight: 16,
|
|
188
|
+
},
|
|
189
|
+
verticalList: {
|
|
190
|
+
paddingBottom: 16,
|
|
191
|
+
},
|
|
192
|
+
emptyContainer: {
|
|
193
|
+
flex: 1,
|
|
194
|
+
justifyContent: "center",
|
|
195
|
+
alignItems: "center",
|
|
196
|
+
padding: 20,
|
|
197
|
+
},
|
|
198
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* User preferences, theme, language, notifications
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
|
-
* import { useSettings,
|
|
8
|
+
* import { useSettings, SettingsScreen, AppearanceScreen, SettingsItemCard, DisclaimerSetting } from '@umituz/react-native-settings';
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
// =============================================================================
|
|
@@ -74,9 +74,6 @@ export type {
|
|
|
74
74
|
// PRESENTATION LAYER - Components
|
|
75
75
|
// =============================================================================
|
|
76
76
|
|
|
77
|
-
export { SettingItem } from './presentation/components/SettingItem';
|
|
78
|
-
export type { SettingItemProps } from './presentation/components/SettingItem';
|
|
79
|
-
|
|
80
77
|
export { SettingsItemCard } from './presentation/components/SettingsItemCard';
|
|
81
78
|
export type { SettingsItemCardProps } from './presentation/components/SettingsItemCard';
|
|
82
79
|
|
|
@@ -88,14 +85,6 @@ export type { SettingsFooterProps } from './presentation/components/SettingsFoot
|
|
|
88
85
|
|
|
89
86
|
export { SettingsErrorBoundary } from './presentation/components/SettingsErrorBoundary';
|
|
90
87
|
|
|
91
|
-
export { StorageClearSetting } from './presentation/components/StorageClearSetting';
|
|
92
|
-
export type { StorageClearSettingProps } from './presentation/components/StorageClearSetting';
|
|
93
|
-
|
|
94
|
-
export { CloudSyncSetting } from './presentation/components/CloudSyncSetting';
|
|
95
|
-
export type { CloudSyncSettingProps } from './presentation/components/CloudSyncSetting';
|
|
96
|
-
|
|
97
|
-
export { DevSettingsSection } from './presentation/components/DevSettingsSection';
|
|
98
|
-
export type { DevSettingsProps } from './presentation/components/DevSettingsSection';
|
|
99
88
|
|
|
100
89
|
// =============================================================================
|
|
101
90
|
// DOMAIN EXPORTS - Consolidated Features
|
|
@@ -125,6 +114,12 @@ export * from "./domains/rating";
|
|
|
125
114
|
// Video Tutorials Domain - Learning resources, tutorials
|
|
126
115
|
export * from "./domains/video-tutorials";
|
|
127
116
|
|
|
117
|
+
// Cloud Sync Domain - Cloud synchronization settings
|
|
118
|
+
export * from "./domains/cloud-sync";
|
|
119
|
+
|
|
120
|
+
// Dev Domain - Development-only settings (DEV mode)
|
|
121
|
+
export * from "./domains/dev";
|
|
122
|
+
|
|
128
123
|
// =============================================================================
|
|
129
124
|
// PRESENTATION LAYER - Re-exports from Dependencies
|
|
130
125
|
// =============================================================================
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { View, Pressable, StyleSheet, ViewStyle } from "react-native";
|
|
2
|
+
import { View, Pressable, StyleSheet, ViewStyle, Switch } from "react-native";
|
|
3
3
|
import {
|
|
4
4
|
useAppDesignTokens,
|
|
5
5
|
AtomicIcon,
|
|
@@ -28,6 +28,14 @@ export interface SettingsItemCardProps {
|
|
|
28
28
|
iconColor?: string;
|
|
29
29
|
/** Show chevron even if not clickable */
|
|
30
30
|
showChevron?: boolean;
|
|
31
|
+
/** Show switch instead of chevron */
|
|
32
|
+
showSwitch?: boolean;
|
|
33
|
+
/** Switch value (when showSwitch is true) */
|
|
34
|
+
switchValue?: boolean;
|
|
35
|
+
/** Switch change handler */
|
|
36
|
+
onSwitchChange?: (value: boolean) => void;
|
|
37
|
+
/** Disable the item */
|
|
38
|
+
disabled?: boolean;
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
/**
|
|
@@ -47,14 +55,40 @@ export const SettingsItemCard: React.FC<SettingsItemCardProps> = ({
|
|
|
47
55
|
iconBgColor,
|
|
48
56
|
iconColor,
|
|
49
57
|
showChevron,
|
|
58
|
+
showSwitch = false,
|
|
59
|
+
switchValue,
|
|
60
|
+
onSwitchChange,
|
|
61
|
+
disabled = false,
|
|
50
62
|
}) => {
|
|
51
63
|
const tokens = useAppDesignTokens();
|
|
52
64
|
const colors = tokens.colors;
|
|
53
65
|
|
|
54
66
|
const defaultIconBg = iconBgColor || `${colors.primary}15`;
|
|
55
67
|
const defaultIconColor = iconColor || colors.primary;
|
|
56
|
-
const isClickable = !!onPress;
|
|
57
|
-
const shouldShowChevron = showChevron ?? isClickable;
|
|
68
|
+
const isClickable = !!onPress && !showSwitch;
|
|
69
|
+
const shouldShowChevron = !showSwitch && (showChevron ?? isClickable);
|
|
70
|
+
|
|
71
|
+
const renderRightElement = () => {
|
|
72
|
+
if (showSwitch) {
|
|
73
|
+
return (
|
|
74
|
+
<Switch
|
|
75
|
+
value={switchValue}
|
|
76
|
+
onValueChange={onSwitchChange}
|
|
77
|
+
trackColor={{
|
|
78
|
+
false: colors.surfaceVariant,
|
|
79
|
+
true: colors.primary,
|
|
80
|
+
}}
|
|
81
|
+
thumbColor={colors.surface}
|
|
82
|
+
ios_backgroundColor={colors.surfaceVariant}
|
|
83
|
+
disabled={disabled}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (shouldShowChevron) {
|
|
88
|
+
return <AtomicIcon name={rightIcon} size="sm" color="secondary" />;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
};
|
|
58
92
|
|
|
59
93
|
const renderContent = () => (
|
|
60
94
|
<View style={styles.content}>
|
|
@@ -64,9 +98,9 @@ export const SettingsItemCard: React.FC<SettingsItemCardProps> = ({
|
|
|
64
98
|
<View style={styles.textContainer}>
|
|
65
99
|
<AtomicText
|
|
66
100
|
type="bodyLarge"
|
|
67
|
-
color="onSurface"
|
|
101
|
+
color={disabled ? "surfaceVariant" : "onSurface"}
|
|
68
102
|
numberOfLines={1}
|
|
69
|
-
style={{ marginBottom: tokens.spacing.xs }}
|
|
103
|
+
style={{ marginBottom: description ? tokens.spacing.xs : 0, opacity: disabled ? 0.6 : 1 }}
|
|
70
104
|
>
|
|
71
105
|
{title}
|
|
72
106
|
</AtomicText>
|
|
@@ -76,9 +110,7 @@ export const SettingsItemCard: React.FC<SettingsItemCardProps> = ({
|
|
|
76
110
|
</AtomicText>
|
|
77
111
|
)}
|
|
78
112
|
</View>
|
|
79
|
-
{
|
|
80
|
-
<AtomicIcon name={rightIcon} size="sm" color="secondary" />
|
|
81
|
-
)}
|
|
113
|
+
{renderRightElement()}
|
|
82
114
|
</View>
|
|
83
115
|
);
|
|
84
116
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { SettingsConfig, CustomSettingsSection } from "../screens/types";
|
|
6
|
-
import type { DevSettingsProps } from "
|
|
6
|
+
import type { DevSettingsProps } from "../../domains/dev";
|
|
7
7
|
import type { FAQCategory } from "../../domains/faqs";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -16,7 +16,7 @@ import { SettingsErrorBoundary } from "../components/SettingsErrorBoundary";
|
|
|
16
16
|
import { normalizeSettingsConfig } from "./utils/normalizeConfig";
|
|
17
17
|
import { useFeatureDetection } from "./hooks/useFeatureDetection";
|
|
18
18
|
import type { SettingsConfig, CustomSettingsSection } from "./types";
|
|
19
|
-
import type { DevSettingsProps } from "
|
|
19
|
+
import type { DevSettingsProps } from "../../domains/dev";
|
|
20
20
|
|
|
21
21
|
export interface SettingsScreenProps {
|
|
22
22
|
config?: SettingsConfig;
|
|
@@ -3,7 +3,7 @@ import { View, StyleSheet } from "react-native";
|
|
|
3
3
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
4
4
|
import { SettingsFooter } from "../../components/SettingsFooter";
|
|
5
5
|
import { SettingsSection } from "../../components/SettingsSection";
|
|
6
|
-
import { DevSettingsSection, DevSettingsProps } from "
|
|
6
|
+
import { DevSettingsSection, DevSettingsProps } from "../../../domains/dev";
|
|
7
7
|
import { DisclaimerSetting } from "../../../domains/disclaimer";
|
|
8
8
|
import { ProfileSectionLoader } from "./sections/ProfileSectionLoader";
|
|
9
9
|
import { FeatureSettingsSection } from "./sections/FeatureSettingsSection";
|
|
@@ -3,7 +3,7 @@ import { useLocalization } from "@umituz/react-native-localization";
|
|
|
3
3
|
import { SupportSection } from "../../../../domains/feedback/presentation/components/SupportSection";
|
|
4
4
|
import { FAQSection } from "../../../../domains/faqs/presentation/components/FAQSection";
|
|
5
5
|
import { SettingsSection } from "../../../components/SettingsSection";
|
|
6
|
-
import {
|
|
6
|
+
import { SettingsItemCard } from "../../../components/SettingsItemCard";
|
|
7
7
|
|
|
8
8
|
interface SupportSettingsSectionProps {
|
|
9
9
|
features: any;
|
|
@@ -23,7 +23,7 @@ export const SupportSettingsSection: React.FC<SupportSettingsSectionProps> = ({
|
|
|
23
23
|
{(features.feedback || features.rating) && (
|
|
24
24
|
<SupportSection
|
|
25
25
|
renderSection={(props: any) => <>{props.children}</>}
|
|
26
|
-
renderItem={(props: any) => <
|
|
26
|
+
renderItem={(props: any) => <SettingsItemCard title={props.title} icon={props.icon} onPress={props.onPress} />}
|
|
27
27
|
feedbackConfig={{
|
|
28
28
|
enabled: features.feedback,
|
|
29
29
|
config: {
|
|
@@ -70,7 +70,7 @@ export const SupportSettingsSection: React.FC<SupportSettingsSectionProps> = ({
|
|
|
70
70
|
{features.faqs && (
|
|
71
71
|
<FAQSection
|
|
72
72
|
renderSection={(props: any) => <>{props.children}</>}
|
|
73
|
-
renderItem={(props: any) => <
|
|
73
|
+
renderItem={(props: any) => <SettingsItemCard title={props.title} icon={props.icon} onPress={props.onPress} />}
|
|
74
74
|
config={{
|
|
75
75
|
enabled: features.faqs,
|
|
76
76
|
...normalizedConfig.faqs.config,
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Video Tutorial Service
|
|
3
|
-
* Single Responsibility: Handle video tutorial data operations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
collection,
|
|
8
|
-
getDocs,
|
|
9
|
-
query,
|
|
10
|
-
orderBy,
|
|
11
|
-
where,
|
|
12
|
-
limit,
|
|
13
|
-
} from "firebase/firestore";
|
|
14
|
-
import type { Firestore, QueryConstraint } from "firebase/firestore";
|
|
15
|
-
import type {
|
|
16
|
-
VideoTutorial,
|
|
17
|
-
VideoTutorialCategory,
|
|
18
|
-
VideoTutorialFilters,
|
|
19
|
-
} from "../../types";
|
|
20
|
-
|
|
21
|
-
export interface VideoTutorialServiceConfig {
|
|
22
|
-
db?: Firestore;
|
|
23
|
-
collectionName?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
class VideoTutorialService {
|
|
27
|
-
private config: VideoTutorialServiceConfig = {
|
|
28
|
-
collectionName: "video_tutorials",
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
initialize(config: VideoTutorialServiceConfig) {
|
|
32
|
-
this.config = { ...this.config, ...config };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
private get db(): Firestore {
|
|
36
|
-
if (!this.config.db) {
|
|
37
|
-
throw new Error("VideoTutorialService: Firestore not initialized. Call initialize({ db }) first.");
|
|
38
|
-
}
|
|
39
|
-
return this.config.db;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
private get collectionName(): string {
|
|
43
|
-
return this.config.collectionName || "video_tutorials";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async getAllTutorials(
|
|
47
|
-
filters?: VideoTutorialFilters,
|
|
48
|
-
): Promise<VideoTutorial[]> {
|
|
49
|
-
const constraints: QueryConstraint[] = [];
|
|
50
|
-
|
|
51
|
-
if (filters?.category) {
|
|
52
|
-
constraints.push(where("category", "==", filters.category));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (filters?.difficulty) {
|
|
56
|
-
constraints.push(where("difficulty", "==", filters.difficulty));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (filters?.featured !== undefined) {
|
|
60
|
-
constraints.push(where("featured", "==", filters.featured));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
constraints.push(orderBy("createdAt", "desc"));
|
|
64
|
-
|
|
65
|
-
const q = query(collection(this.db, this.collectionName), ...constraints);
|
|
66
|
-
const snapshot = await getDocs(q);
|
|
67
|
-
|
|
68
|
-
return snapshot.docs.map((doc) => this.mapDocumentToTutorial(doc));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async getFeaturedTutorials(maxCount: number = 5): Promise<VideoTutorial[]> {
|
|
72
|
-
const q = query(
|
|
73
|
-
collection(this.db, this.collectionName),
|
|
74
|
-
where("featured", "==", true),
|
|
75
|
-
orderBy("createdAt", "desc"),
|
|
76
|
-
limit(maxCount),
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const snapshot = await getDocs(q);
|
|
80
|
-
return snapshot.docs.map((doc) => this.mapDocumentToTutorial(doc));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async getTutorialsByCategory(
|
|
84
|
-
category: VideoTutorialCategory,
|
|
85
|
-
): Promise<VideoTutorial[]> {
|
|
86
|
-
const q = query(
|
|
87
|
-
collection(this.db, this.collectionName),
|
|
88
|
-
where("category", "==", category),
|
|
89
|
-
orderBy("createdAt", "desc"),
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
const snapshot = await getDocs(q);
|
|
93
|
-
return snapshot.docs.map((doc) => this.mapDocumentToTutorial(doc));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
private mapDocumentToTutorial(doc: any): VideoTutorial {
|
|
97
|
-
const data = doc.data();
|
|
98
|
-
return {
|
|
99
|
-
id: doc.id,
|
|
100
|
-
title: data.title || "",
|
|
101
|
-
description: data.description || "",
|
|
102
|
-
videoUrl: data.videoUrl || "",
|
|
103
|
-
thumbnailUrl: data.thumbnailUrl || "",
|
|
104
|
-
duration: data.duration || 0,
|
|
105
|
-
category: data.category || "getting-started",
|
|
106
|
-
difficulty: data.difficulty || "beginner",
|
|
107
|
-
featured: data.featured || false,
|
|
108
|
-
tags: data.tags || [],
|
|
109
|
-
createdAt: data.createdAt?.toDate() || new Date(),
|
|
110
|
-
updatedAt: data.updatedAt?.toDate() || new Date(),
|
|
111
|
-
viewCount: data.viewCount || 0,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export const videoTutorialService = new VideoTutorialService();
|
|
117
|
-
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Video Tutorial Hooks
|
|
3
|
-
* Single Responsibility: Provide React hooks for video tutorials
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useQuery } from "@tanstack/react-query";
|
|
7
|
-
import { videoTutorialService } from "../../infrastructure/services/video-tutorial.service";
|
|
8
|
-
import type {
|
|
9
|
-
VideoTutorial,
|
|
10
|
-
VideoTutorialCategory,
|
|
11
|
-
VideoTutorialFilters,
|
|
12
|
-
} from "../../types";
|
|
13
|
-
|
|
14
|
-
export function useVideoTutorials(filters?: VideoTutorialFilters) {
|
|
15
|
-
return useQuery({
|
|
16
|
-
queryKey: ["video-tutorials", filters],
|
|
17
|
-
queryFn: () => videoTutorialService.getAllTutorials(filters),
|
|
18
|
-
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function useFeaturedTutorials(maxCount: number = 5) {
|
|
23
|
-
return useQuery({
|
|
24
|
-
queryKey: ["featured-tutorials", maxCount],
|
|
25
|
-
queryFn: () => videoTutorialService.getFeaturedTutorials(maxCount),
|
|
26
|
-
staleTime: 10 * 60 * 1000, // 10 minutes
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function useTutorialsByCategory(category: VideoTutorialCategory) {
|
|
31
|
-
return useQuery({
|
|
32
|
-
queryKey: ["tutorials-by-category", category],
|
|
33
|
-
queryFn: () => videoTutorialService.getTutorialsByCategory(category),
|
|
34
|
-
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
35
|
-
});
|
|
36
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { View, StyleSheet, Switch } from "react-native";
|
|
3
|
-
import {
|
|
4
|
-
AtomicIcon,
|
|
5
|
-
AtomicText,
|
|
6
|
-
useAppDesignTokens,
|
|
7
|
-
} from "@umituz/react-native-design-system";
|
|
8
|
-
import { SettingsItemCard } from "./SettingsItemCard";
|
|
9
|
-
|
|
10
|
-
export interface SettingItemProps {
|
|
11
|
-
icon: string;
|
|
12
|
-
title: string;
|
|
13
|
-
value?: string;
|
|
14
|
-
onPress?: () => void;
|
|
15
|
-
showSwitch?: boolean;
|
|
16
|
-
switchValue?: boolean;
|
|
17
|
-
onSwitchChange?: (value: boolean) => void;
|
|
18
|
-
isLast?: boolean;
|
|
19
|
-
iconColor?: string;
|
|
20
|
-
titleColor?: string;
|
|
21
|
-
testID?: string;
|
|
22
|
-
disabled?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* SettingItem - Enhanced ListItem for Settings
|
|
27
|
-
* Uses design system's ListItem molecule with custom switch support
|
|
28
|
-
*/
|
|
29
|
-
export const SettingItem: React.FC<SettingItemProps> = ({
|
|
30
|
-
icon,
|
|
31
|
-
title,
|
|
32
|
-
value,
|
|
33
|
-
onPress,
|
|
34
|
-
showSwitch = false,
|
|
35
|
-
switchValue,
|
|
36
|
-
onSwitchChange,
|
|
37
|
-
isLast = false,
|
|
38
|
-
iconColor,
|
|
39
|
-
titleColor,
|
|
40
|
-
testID,
|
|
41
|
-
disabled = false,
|
|
42
|
-
}) => {
|
|
43
|
-
const tokens = useAppDesignTokens();
|
|
44
|
-
|
|
45
|
-
// For switch items, we need custom rendering
|
|
46
|
-
if (showSwitch) {
|
|
47
|
-
return (
|
|
48
|
-
<View
|
|
49
|
-
style={[
|
|
50
|
-
styles.switchContainer,
|
|
51
|
-
{
|
|
52
|
-
borderBottomWidth: isLast ? 0 : 1,
|
|
53
|
-
borderBottomColor: `${tokens.colors.onSurface}10`,
|
|
54
|
-
}
|
|
55
|
-
]}
|
|
56
|
-
>
|
|
57
|
-
<View style={styles.switchContent}>
|
|
58
|
-
<View style={[
|
|
59
|
-
styles.iconWrapper,
|
|
60
|
-
{ backgroundColor: iconColor ? `${iconColor}15` : `${tokens.colors.primary}15` }
|
|
61
|
-
]}>
|
|
62
|
-
<AtomicIcon
|
|
63
|
-
name={icon}
|
|
64
|
-
size="md"
|
|
65
|
-
customColor={iconColor || tokens.colors.primary}
|
|
66
|
-
/>
|
|
67
|
-
</View>
|
|
68
|
-
<View style={styles.textContainer}>
|
|
69
|
-
<AtomicText
|
|
70
|
-
type="bodyLarge"
|
|
71
|
-
color={disabled ? "surfaceVariant" : "onSurface"}
|
|
72
|
-
style={[
|
|
73
|
-
titleColor ? { color: titleColor } : {},
|
|
74
|
-
{ opacity: disabled ? 0.6 : 1 }
|
|
75
|
-
]}
|
|
76
|
-
numberOfLines={1}
|
|
77
|
-
>
|
|
78
|
-
{title}
|
|
79
|
-
</AtomicText>
|
|
80
|
-
{value && (
|
|
81
|
-
<AtomicText
|
|
82
|
-
type="bodySmall"
|
|
83
|
-
color="secondary"
|
|
84
|
-
numberOfLines={2}
|
|
85
|
-
style={{ marginTop: 2 }}
|
|
86
|
-
>
|
|
87
|
-
{value}
|
|
88
|
-
</AtomicText>
|
|
89
|
-
)}
|
|
90
|
-
</View>
|
|
91
|
-
</View>
|
|
92
|
-
<Switch
|
|
93
|
-
value={switchValue}
|
|
94
|
-
onValueChange={onSwitchChange}
|
|
95
|
-
trackColor={{
|
|
96
|
-
false: tokens.colors.surfaceVariant,
|
|
97
|
-
true: tokens.colors.primary
|
|
98
|
-
}}
|
|
99
|
-
thumbColor={tokens.colors.surface}
|
|
100
|
-
ios_backgroundColor={tokens.colors.surfaceVariant}
|
|
101
|
-
disabled={disabled}
|
|
102
|
-
/>
|
|
103
|
-
</View>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Use SettingsItemCard for regular items
|
|
108
|
-
return (
|
|
109
|
-
<SettingsItemCard
|
|
110
|
-
title={title}
|
|
111
|
-
description={value}
|
|
112
|
-
icon={icon as any}
|
|
113
|
-
onPress={onPress}
|
|
114
|
-
iconColor={iconColor}
|
|
115
|
-
/>
|
|
116
|
-
);
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const styles = StyleSheet.create({
|
|
120
|
-
switchContainer: {
|
|
121
|
-
flexDirection: "row",
|
|
122
|
-
alignItems: "center",
|
|
123
|
-
justifyContent: "space-between",
|
|
124
|
-
paddingHorizontal: 16,
|
|
125
|
-
paddingVertical: 12,
|
|
126
|
-
minHeight: 64,
|
|
127
|
-
},
|
|
128
|
-
switchContent: {
|
|
129
|
-
flexDirection: "row",
|
|
130
|
-
alignItems: "center",
|
|
131
|
-
flex: 1,
|
|
132
|
-
},
|
|
133
|
-
iconWrapper: {
|
|
134
|
-
width: 40,
|
|
135
|
-
height: 40,
|
|
136
|
-
borderRadius: 10,
|
|
137
|
-
justifyContent: "center",
|
|
138
|
-
alignItems: "center",
|
|
139
|
-
marginRight: 12,
|
|
140
|
-
},
|
|
141
|
-
textContainer: {
|
|
142
|
-
flex: 1,
|
|
143
|
-
paddingRight: 8,
|
|
144
|
-
},
|
|
145
|
-
});
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for SettingItem Component
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import React from 'react';
|
|
6
|
-
import { render, fireEvent } from '@testing-library/react-native';
|
|
7
|
-
import { SettingItem } from '../SettingItem';
|
|
8
|
-
|
|
9
|
-
// Mock lucide-react-native
|
|
10
|
-
jest.mock('lucide-react-native', () => ({
|
|
11
|
-
Bell: 'Bell',
|
|
12
|
-
Palette: 'Palette',
|
|
13
|
-
ChevronRight: 'ChevronRight',
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
// Mock dependencies
|
|
17
|
-
jest.mock('@umituz/react-native-design-system', () => ({
|
|
18
|
-
useAppDesignTokens: () => ({
|
|
19
|
-
colors: {
|
|
20
|
-
backgroundPrimary: '#ffffff',
|
|
21
|
-
primary: '#007AFF',
|
|
22
|
-
textPrimary: '#000000',
|
|
23
|
-
textSecondary: '#666666',
|
|
24
|
-
},
|
|
25
|
-
spacing: {
|
|
26
|
-
md: 16,
|
|
27
|
-
},
|
|
28
|
-
}),
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
describe('SettingItem', () => {
|
|
32
|
-
it('renders correctly with basic props', () => {
|
|
33
|
-
const { getByText, getByTestId } = render(
|
|
34
|
-
<SettingItem
|
|
35
|
-
icon={Bell}
|
|
36
|
-
title="Test Setting"
|
|
37
|
-
testID="test-setting"
|
|
38
|
-
/>
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
expect(getByText('Test Setting')).toBeTruthy();
|
|
42
|
-
expect(getByTestId('test-setting')).toBeTruthy();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('displays value when provided', () => {
|
|
46
|
-
const { getByText } = render(
|
|
47
|
-
<SettingItem
|
|
48
|
-
icon={Bell}
|
|
49
|
-
title="Test Setting"
|
|
50
|
-
value="Test Value"
|
|
51
|
-
/>
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
expect(getByText('Test Setting')).toBeTruthy();
|
|
55
|
-
expect(getByText('Test Value')).toBeTruthy();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('shows switch when showSwitch is true', () => {
|
|
59
|
-
const { getByTestId, queryByTestId } = render(
|
|
60
|
-
<SettingItem
|
|
61
|
-
icon={Bell}
|
|
62
|
-
title="Test Setting"
|
|
63
|
-
showSwitch={true}
|
|
64
|
-
switchValue={true}
|
|
65
|
-
testID="test-setting"
|
|
66
|
-
/>
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
expect(getByTestId('test-setting')).toBeTruthy();
|
|
70
|
-
// Switch should be present, chevron should not
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('calls onPress when pressed', () => {
|
|
74
|
-
const mockOnPress = jest.fn();
|
|
75
|
-
const { getByTestId } = render(
|
|
76
|
-
<SettingItem
|
|
77
|
-
icon={Bell}
|
|
78
|
-
title="Test Setting"
|
|
79
|
-
onPress={mockOnPress}
|
|
80
|
-
testID="test-setting"
|
|
81
|
-
/>
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
fireEvent.press(getByTestId('test-setting'));
|
|
85
|
-
expect(mockOnPress).toHaveBeenCalledTimes(1);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('calls onSwitchChange when switch is toggled', () => {
|
|
89
|
-
const mockOnSwitchChange = jest.fn();
|
|
90
|
-
const { getByRole } = render(
|
|
91
|
-
<SettingItem
|
|
92
|
-
icon={Bell}
|
|
93
|
-
title="Test Setting"
|
|
94
|
-
showSwitch={true}
|
|
95
|
-
switchValue={false}
|
|
96
|
-
onSwitchChange={mockOnSwitchChange}
|
|
97
|
-
/>
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
const switchElement = getByRole('switch');
|
|
101
|
-
fireEvent(switchElement, 'valueChange', true);
|
|
102
|
-
expect(mockOnSwitchChange).toHaveBeenCalledWith(true);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('applies custom colors', () => {
|
|
106
|
-
const { getByText } = render(
|
|
107
|
-
<SettingItem
|
|
108
|
-
icon={Bell}
|
|
109
|
-
title="Test Setting"
|
|
110
|
-
iconColor="#FF0000"
|
|
111
|
-
titleColor="#00FF00"
|
|
112
|
-
/>
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
expect(getByText('Test Setting')).toBeTruthy();
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('shows disabled state correctly', () => {
|
|
119
|
-
const mockOnPress = jest.fn();
|
|
120
|
-
const { getByTestId } = render(
|
|
121
|
-
<SettingItem
|
|
122
|
-
icon={Bell}
|
|
123
|
-
title="Test Setting"
|
|
124
|
-
disabled={true}
|
|
125
|
-
onPress={mockOnPress}
|
|
126
|
-
testID="test-setting"
|
|
127
|
-
/>
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const pressable = getByTestId('test-setting');
|
|
131
|
-
expect(pressable.props.disabled).toBe(true);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('does not show divider when isLast is true', () => {
|
|
135
|
-
const { queryByTestId } = render(
|
|
136
|
-
<SettingItem
|
|
137
|
-
icon={Bell}
|
|
138
|
-
title="Test Setting"
|
|
139
|
-
isLast={true}
|
|
140
|
-
/>
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
// Divider should not be present
|
|
144
|
-
expect(queryByTestId('setting-divider')).toBeNull();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('renders with different icon types', () => {
|
|
148
|
-
const { getByText } = render(
|
|
149
|
-
<SettingItem
|
|
150
|
-
icon={Palette}
|
|
151
|
-
title="Appearance Setting"
|
|
152
|
-
/>
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
expect(getByText('Appearance Setting')).toBeTruthy();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('handles long text correctly', () => {
|
|
159
|
-
const longTitle = 'This is a very long title that should be truncated properly';
|
|
160
|
-
const longValue = 'This is a very long value that should be truncated properly and should not break the layout';
|
|
161
|
-
|
|
162
|
-
const { getByText } = render(
|
|
163
|
-
<SettingItem
|
|
164
|
-
icon={Bell}
|
|
165
|
-
title={longTitle}
|
|
166
|
-
value={longValue}
|
|
167
|
-
/>
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
expect(getByText(longTitle)).toBeTruthy();
|
|
171
|
-
expect(getByText(longValue)).toBeTruthy();
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('applies custom switch colors', () => {
|
|
175
|
-
const { getByRole } = render(
|
|
176
|
-
<SettingItem
|
|
177
|
-
icon={Bell}
|
|
178
|
-
title="Test Setting"
|
|
179
|
-
showSwitch={true}
|
|
180
|
-
switchValue={true}
|
|
181
|
-
switchThumbColor="#FF0000"
|
|
182
|
-
switchTrackColors={{ false: '#CCCCCC', true: '#00FF00' }}
|
|
183
|
-
/>
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
const switchElement = getByRole('switch');
|
|
187
|
-
expect(switchElement).toBeTruthy();
|
|
188
|
-
});
|
|
189
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { useLocalization } from "@umituz/react-native-localization";
|
|
3
|
-
import type { IconName } from "@umituz/react-native-design-system";
|
|
4
|
-
import { SettingsItemCard } from "../../../components/SettingsItemCard";
|
|
5
|
-
import type { SubscriptionConfig } from "../../types";
|
|
6
|
-
|
|
7
|
-
interface SubscriptionSettingsSectionProps {
|
|
8
|
-
config?: SubscriptionConfig;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const SubscriptionSettingsSection: React.FC<SubscriptionSettingsSectionProps> = ({
|
|
12
|
-
config,
|
|
13
|
-
}) => {
|
|
14
|
-
const { t } = useLocalization();
|
|
15
|
-
|
|
16
|
-
// onPress is required for subscription section to be functional
|
|
17
|
-
if (!config || !config.onPress) return null;
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<SettingsItemCard
|
|
21
|
-
title={config.title || t("settings.subscription.title")}
|
|
22
|
-
description={config.description || t("settings.subscription.description")}
|
|
23
|
-
icon={(config.icon || "star") as IconName}
|
|
24
|
-
onPress={config.onPress}
|
|
25
|
-
sectionTitle={config.sectionTitle || t("settings.sections.subscription")}
|
|
26
|
-
/>
|
|
27
|
-
);
|
|
28
|
-
};
|