@umituz/react-native-settings 5.4.10 → 5.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/package.json +1 -1
  2. package/src/core/index.ts +1 -1
  3. package/src/core/patterns/Screen/useScreenData.ts +0 -1
  4. package/src/core/utils/logger.ts +5 -5
  5. package/src/domains/disclaimer/index.ts +0 -3
  6. package/src/domains/disclaimer/presentation/components/DisclaimerSetting.tsx +18 -43
  7. package/src/domains/disclaimer/presentation/screens/DisclaimerScreen.tsx +42 -92
  8. package/src/domains/feedback/index.ts +2 -2
  9. package/src/domains/feedback/presentation/components/SupportSection.tsx +16 -44
  10. package/src/domains/feedback/presentation/screens/FeatureRequestScreen.tsx +13 -35
  11. package/src/domains/feedback/presentation/screens/FeedbackScreen.tsx +75 -0
  12. package/src/domains/notifications/infrastructure/services/NotificationService.ts +2 -4
  13. package/src/domains/rating/application/services/RatingService.ts +31 -0
  14. package/src/domains/rating/index.ts +3 -3
  15. package/src/domains/rating/presentation/hooks/useAppRating.tsx +42 -65
  16. package/src/domains/rating/presentation/screens/RatingPromptScreen.tsx +162 -0
  17. package/src/infrastructure/services/SettingsService.ts +3 -9
  18. package/src/presentation/components/GenericModal.tsx +3 -7
  19. package/src/presentation/components/GenericScreen.tsx +1 -6
  20. package/src/presentation/navigation/hooks/useSettingsScreens.ts +26 -1
  21. package/src/presentation/navigation/types.ts +6 -0
  22. package/src/domains/disclaimer/presentation/hooks/useDisclaimerModal.ts +0 -72
  23. package/src/domains/feedback/presentation/hooks/useFeedbackModal.ts +0 -182
  24. package/src/domains/rating/presentation/hooks/useRatingPromptModal.ts +0 -122
@@ -34,11 +34,11 @@ export { StarRating } from './presentation/components/StarRating';
34
34
  export type { StarRatingProps } from './presentation/components/StarRating';
35
35
 
36
36
  // =============================================================================
37
- // PRESENTATION LAYER - Components (App Store Rating)
37
+ // PRESENTATION LAYER - Screens
38
38
  // =============================================================================
39
39
 
40
- export { useRatingPromptModal } from './presentation/hooks/useRatingPromptModal';
41
- export type { RatingPromptOptions } from './presentation/hooks/useRatingPromptModal';
40
+ export { RatingPromptScreen } from './presentation/screens/RatingPromptScreen';
41
+ export type { RatingPromptScreenProps, RatingPromptScreenParams } from './presentation/screens/RatingPromptScreen';
42
42
 
43
43
  // =============================================================================
44
44
  // PRESENTATION LAYER - Hooks (App Store Rating)
@@ -4,7 +4,7 @@
4
4
  * Lazy loads expo-store-review to reduce bundle size
5
5
  */
6
6
 
7
- import React, { useState, useCallback, useMemo } from "react";
7
+ import { useCallback, useMemo } from "react";
8
8
  import type {
9
9
  RatingConfig,
10
10
  UseAppRatingResult,
@@ -12,14 +12,14 @@ import type {
12
12
  } from "../../domain/entities/RatingConfig";
13
13
  import { DEFAULT_RATING_CONFIG } from "../../domain/entities/RatingConfig";
14
14
  import * as RatingService from "../../application/services/RatingService";
15
- import { RatingPromptModal } from "../components/RatingPromptModal";
15
+ import { useAppNavigation } from "@umituz/react-native-design-system/molecules";
16
16
  import { isDev } from "../../../../utils/devUtils";
17
17
 
18
18
  /**
19
- * App rating hook with 2-step prompt flow
19
+ * App rating hook with navigation-based prompt flow
20
20
  */
21
21
  export function useAppRating(config: RatingConfig): UseAppRatingResult {
22
- const [isVisible, setIsVisible] = useState(false);
22
+ const navigation = useAppNavigation();
23
23
 
24
24
  const mergedConfig = useMemo<RatingConfig>(() => ({
25
25
  ...DEFAULT_RATING_CONFIG,
@@ -35,13 +35,48 @@ export function useAppRating(config: RatingConfig): UseAppRatingResult {
35
35
  }, [mergedConfig]);
36
36
 
37
37
  const showPrompt = useCallback(async (): Promise<void> => {
38
- setIsVisible(true);
39
38
  await RatingService.markPromptShown(mergedConfig.eventType);
40
39
 
40
+ navigation.push('RatingPrompt' as never, {
41
+ appName: mergedConfig.appName,
42
+ translations: mergedConfig.translations,
43
+ onPositive: async () => {
44
+ await RatingService.markRated();
45
+
46
+ try {
47
+ // Lazy load expo-store-review
48
+ const StoreReview = await import('expo-store-review');
49
+ const isAvailable = await StoreReview.isAvailableAsync();
50
+
51
+ if (isAvailable) {
52
+ await StoreReview.requestReview();
53
+ }
54
+ } catch (error) {
55
+ if (isDev()) {
56
+ console.error('[useAppRating] Failed to request review:', error);
57
+ }
58
+ }
59
+
60
+ if (mergedConfig.onPositiveFeedback) {
61
+ await mergedConfig.onPositiveFeedback();
62
+ }
63
+ },
64
+ onNegative: async () => {
65
+ if (mergedConfig.onNegativeFeedback) {
66
+ await mergedConfig.onNegativeFeedback();
67
+ }
68
+ },
69
+ onLater: async () => {
70
+ if (mergedConfig.onPromptDismissed) {
71
+ await mergedConfig.onPromptDismissed();
72
+ }
73
+ },
74
+ });
75
+
41
76
  if (mergedConfig.onPromptShown) {
42
77
  await mergedConfig.onPromptShown();
43
78
  }
44
- }, [mergedConfig]);
79
+ }, [mergedConfig, navigation]);
45
80
 
46
81
  const checkAndShow = useCallback(async (): Promise<boolean> => {
47
82
  const should = await shouldShow();
@@ -62,62 +97,6 @@ export function useAppRating(config: RatingConfig): UseAppRatingResult {
62
97
  return RatingService.getState(mergedConfig.eventType);
63
98
  }, [mergedConfig.eventType]);
64
99
 
65
- const handlePositive = useCallback(async () => {
66
- setIsVisible(false);
67
- await RatingService.markRated();
68
-
69
- try {
70
- // Lazy load expo-store-review
71
- const StoreReview = await import('expo-store-review');
72
- const isAvailable = await StoreReview.isAvailableAsync();
73
-
74
- if (isAvailable) {
75
- await StoreReview.requestReview();
76
- }
77
- } catch (error) {
78
- if (isDev()) {
79
- console.error('[useAppRating] Failed to request review:', error);
80
- }
81
- }
82
-
83
- if (mergedConfig.onPositiveFeedback) {
84
- await mergedConfig.onPositiveFeedback();
85
- }
86
- }, [mergedConfig]);
87
-
88
- const handleNegative = useCallback(async () => {
89
- setIsVisible(false);
90
-
91
- if (mergedConfig.onNegativeFeedback) {
92
- await mergedConfig.onNegativeFeedback();
93
- }
94
- }, [mergedConfig]);
95
-
96
- const handleLater = useCallback(() => {
97
- setIsVisible(false);
98
- }, []);
99
-
100
- const handleDismiss = useCallback(async () => {
101
- setIsVisible(false);
102
- await RatingService.markDismissed();
103
-
104
- if (mergedConfig.onPromptDismissed) {
105
- await mergedConfig.onPromptDismissed();
106
- }
107
- }, [mergedConfig]);
108
-
109
- const modal = (
110
- <RatingPromptModal
111
- visible={isVisible}
112
- onPositive={handlePositive}
113
- onNegative={handleNegative}
114
- onLater={handleLater}
115
- onDismiss={handleDismiss}
116
- translations={mergedConfig.translations}
117
- appName={mergedConfig.appName}
118
- />
119
- );
120
-
121
100
  return {
122
101
  trackEvent,
123
102
  checkAndShow,
@@ -125,7 +104,5 @@ export function useAppRating(config: RatingConfig): UseAppRatingResult {
125
104
  showPrompt,
126
105
  reset,
127
106
  getState,
128
- isVisible,
129
- modal,
130
- } as UseAppRatingResult & { modal: React.ReactNode };
107
+ } as UseAppRatingResult;
131
108
  }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Rating Prompt Screen
3
+ *
4
+ * Full screen for app rating prompt.
5
+ * Replaces modal approach with native navigation.
6
+ */
7
+
8
+ import React from 'react';
9
+ import { View, StyleSheet, ScrollView } from 'react-native';
10
+ import { ScreenLayout } from '@umituz/react-native-design-system/layouts';
11
+ import { AtomicText, AtomicButton, AtomicIcon } from '@umituz/react-native-design-system/atoms';
12
+ import { NavigationHeader, useAppNavigation } from '@umituz/react-native-design-system/molecules';
13
+ import { useAppDesignTokens } from '@umituz/react-native-design-system/theme';
14
+ import type { RatingTranslations } from '../../domain/entities/RatingConfig';
15
+
16
+ export interface RatingPromptScreenParams {
17
+ appName?: string;
18
+ translations?: RatingTranslations;
19
+ onPositive?: () => void;
20
+ onNegative?: () => void;
21
+ onLater?: () => void;
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ export interface RatingPromptScreenProps {
26
+ route: {
27
+ params: RatingPromptScreenParams;
28
+ };
29
+ }
30
+
31
+ export const RatingPromptScreen: React.FC<RatingPromptScreenProps> = ({ route }) => {
32
+ const navigation = useAppNavigation();
33
+ const tokens = useAppDesignTokens();
34
+ const styles = getStyles(tokens);
35
+ const { appName = 'this app', translations, onPositive, onNegative, onLater } = route.params;
36
+
37
+ const defaultTranslations: RatingTranslations = {
38
+ title: translations?.title ?? 'Enjoying the app?',
39
+ message:
40
+ translations?.message ??
41
+ `If you love using ${appName}, would you mind taking a moment to rate it?`,
42
+ positiveButton: translations?.positiveButton ?? 'Yes, I love it!',
43
+ negativeButton: translations?.negativeButton ?? 'Not really',
44
+ laterButton: translations?.laterButton ?? 'Maybe later',
45
+ };
46
+
47
+ const handlePositive = async () => {
48
+ try {
49
+ // Lazy load expo-store-review
50
+ const StoreReview = await import('expo-store-review');
51
+ const isAvailable = await StoreReview.isAvailableAsync();
52
+
53
+ if (isAvailable) {
54
+ await StoreReview.requestReview();
55
+ }
56
+ } catch (error) {
57
+ console.error('[RatingPromptScreen] Failed to request review:', error);
58
+ }
59
+
60
+ if (onPositive) {
61
+ onPositive();
62
+ }
63
+ navigation.goBack();
64
+ };
65
+
66
+ const handleNegative = () => {
67
+ if (onNegative) {
68
+ onNegative();
69
+ }
70
+ navigation.goBack();
71
+ };
72
+
73
+ const handleLater = () => {
74
+ if (onLater) {
75
+ onLater();
76
+ }
77
+ navigation.goBack();
78
+ };
79
+
80
+ return (
81
+ <ScreenLayout
82
+ scrollable={true}
83
+ edges={['top', 'bottom', 'left', 'right']}
84
+ hideScrollIndicator={false}
85
+ >
86
+ <NavigationHeader title="" />
87
+ <ScrollView
88
+ style={styles.scrollView}
89
+ contentContainerStyle={styles.scrollContent}
90
+ >
91
+ <View style={styles.iconContainer}>
92
+ <AtomicIcon name="star" size="xxl" color="primary" />
93
+ </View>
94
+
95
+ <AtomicText style={styles.title}>{defaultTranslations.title}</AtomicText>
96
+
97
+ <AtomicText style={styles.message}>{defaultTranslations.message}</AtomicText>
98
+
99
+ <View style={styles.buttonContainer}>
100
+ <AtomicButton
101
+ variant="primary"
102
+ onPress={handlePositive}
103
+ style={styles.button}
104
+ >
105
+ {defaultTranslations.positiveButton}
106
+ </AtomicButton>
107
+
108
+ <AtomicButton
109
+ variant="outline"
110
+ onPress={handleNegative}
111
+ style={styles.button}
112
+ >
113
+ {defaultTranslations.negativeButton}
114
+ </AtomicButton>
115
+
116
+ <AtomicButton
117
+ variant="text"
118
+ onPress={handleLater}
119
+ style={styles.button}
120
+ >
121
+ {defaultTranslations.laterButton}
122
+ </AtomicButton>
123
+ </View>
124
+ </ScrollView>
125
+ </ScreenLayout>
126
+ );
127
+ };
128
+
129
+ const getStyles = (_tokens: ReturnType<typeof useAppDesignTokens>) =>
130
+ StyleSheet.create({
131
+ scrollView: {
132
+ flex: 1,
133
+ },
134
+ scrollContent: {
135
+ padding: 20,
136
+ alignItems: 'center',
137
+ justifyContent: 'center',
138
+ minHeight: '100%',
139
+ },
140
+ iconContainer: {
141
+ marginBottom: 24,
142
+ },
143
+ title: {
144
+ fontSize: 24,
145
+ fontWeight: '700',
146
+ textAlign: 'center',
147
+ marginBottom: 16,
148
+ },
149
+ message: {
150
+ fontSize: 16,
151
+ textAlign: 'center',
152
+ marginBottom: 32,
153
+ lineHeight: 24,
154
+ },
155
+ buttonContainer: {
156
+ width: '100%',
157
+ gap: 12,
158
+ },
159
+ button: {
160
+ width: '100%',
161
+ },
162
+ });
@@ -19,21 +19,15 @@ export class SettingsService extends BaseService {
19
19
  }
20
20
 
21
21
  async getSettings(userId: string): Promise<SettingsResult<UserSettings>> {
22
- return this.execute('getSettings', async () => {
23
- return await this.repository.getSettings(userId);
24
- });
22
+ return await this.repository.getSettings(userId);
25
23
  }
26
24
 
27
25
  async saveSettings(settings: UserSettings): Promise<SettingsResult<void>> {
28
- return this.execute('saveSettings', async () => {
29
- return await this.repository.saveSettings(settings);
30
- });
26
+ return await this.repository.saveSettings(settings);
31
27
  }
32
28
 
33
29
  async resetSettings(userId: string): Promise<SettingsResult<void>> {
34
- return this.execute('resetSettings', async () => {
35
- return await this.repository.deleteSettings(userId);
36
- });
30
+ return await this.repository.deleteSettings(userId);
37
31
  }
38
32
  }
39
33
 
@@ -18,12 +18,12 @@
18
18
  */
19
19
 
20
20
  import React, { useMemo } from 'react';
21
- import { View, StyleSheet, TouchableOpacity } from 'react-native';
21
+ import { View, StyleSheet } from 'react-native';
22
22
  import { BaseModal } from '@umituz/react-native-design-system/molecules';
23
23
  import { AtomicText, AtomicButton, AtomicIcon } from '@umituz/react-native-design-system/atoms';
24
24
  import { useAppDesignTokens } from '@umituz/react-native-design-system/theme';
25
25
  import { useResponsive } from '@umituz/react-native-design-system/responsive';
26
- import type { ModalConfig, ModalState } from '../../core/patterns/Modal/ModalConfig';
26
+ import type { ModalState } from '../../core/patterns/Modal/ModalConfig';
27
27
 
28
28
  export interface GenericModalProps {
29
29
  /**
@@ -68,8 +68,6 @@ export const GenericModal: React.FC<GenericModalProps> = ({
68
68
  footer,
69
69
  actions = [],
70
70
  dismissible = true,
71
- closeOnBackdropPress = true,
72
- closeOnBackPress = true,
73
71
  maxWidth,
74
72
  } = config;
75
73
 
@@ -85,8 +83,6 @@ export const GenericModal: React.FC<GenericModalProps> = ({
85
83
  <BaseModal
86
84
  visible={visible}
87
85
  onClose={handleDismiss}
88
- closeOnBackdropPress={closeOnBackdropPress}
89
- closeOnBackPress={closeOnBackPress}
90
86
  >
91
87
  <View
92
88
  style={[
@@ -113,7 +109,7 @@ export const GenericModal: React.FC<GenericModalProps> = ({
113
109
  <AtomicIcon
114
110
  name={icon}
115
111
  size="xl"
116
- color={iconColor || 'primary'}
112
+ color={iconColor as any || 'primary'}
117
113
  />
118
114
  </View>
119
115
  )}
@@ -20,7 +20,7 @@
20
20
  */
21
21
 
22
22
  import React, { useMemo } from 'react';
23
- import { View, StyleSheet, TouchableOpacity } from 'react-native';
23
+ import { View, StyleSheet } from 'react-native';
24
24
  import { ScreenLayout } from '@umituz/react-native-design-system/layouts';
25
25
  import {
26
26
  NavigationHeader,
@@ -88,11 +88,6 @@ export function GenericScreen<T>({
88
88
  <NavigationHeader
89
89
  title={header?.title || ''}
90
90
  onBackPress={showBackButton ? onBackPress : undefined}
91
- actions={header?.actions?.map((action) => ({
92
- icon: action.icon,
93
- onPress: action.onPress,
94
- testID: action.testID,
95
- }))}
96
91
  />
97
92
  ) : undefined;
98
93
 
@@ -3,6 +3,9 @@ import type { StackScreen } from "@umituz/react-native-design-system/molecules";
3
3
  import { LanguageSelectionScreen } from "../../../domains/localization";
4
4
  import { NotificationSettingsScreen } from "../../../domains/notifications";
5
5
  import { SettingsScreen } from "../../screens/SettingsScreen";
6
+ import { DisclaimerScreen } from "../../../domains/disclaimer/presentation/screens/DisclaimerScreen";
7
+ import { FeedbackScreen } from "../../../domains/feedback/presentation/screens/FeedbackScreen";
8
+ import { RatingPromptScreen } from "../../../domains/rating/presentation/screens/RatingPromptScreen";
6
9
 
7
10
  // AccountScreen is an optional peer — lazy require so the package works without @umituz/react-native-auth
8
11
  // Returns null if @umituz/react-native-auth is not installed
@@ -176,6 +179,25 @@ export const useSettingsScreens = (props: UseSettingsScreensProps): StackScreen[
176
179
  }
177
180
  });
178
181
 
182
+ // New screens to replace modals
183
+ const disclaimerScreen = {
184
+ name: "Disclaimer" as const,
185
+ component: DisclaimerScreen,
186
+ options: { headerShown: false },
187
+ };
188
+
189
+ const feedbackScreen = {
190
+ name: "Feedback" as const,
191
+ component: FeedbackScreen,
192
+ options: { headerShown: false },
193
+ };
194
+
195
+ const ratingPromptScreen = {
196
+ name: "RatingPrompt" as const,
197
+ component: RatingPromptScreen,
198
+ options: { headerShown: false },
199
+ };
200
+
179
201
  return combineScreens(
180
202
  baseScreens,
181
203
  faqScreen,
@@ -184,7 +206,10 @@ export const useSettingsScreens = (props: UseSettingsScreensProps): StackScreen[
184
206
  languageScreen,
185
207
  accountScreen,
186
208
  videoTutorialScreen,
187
- featureRequestScreen
209
+ featureRequestScreen,
210
+ disclaimerScreen,
211
+ feedbackScreen,
212
+ ratingPromptScreen
188
213
  );
189
214
  }, [
190
215
  translations,
@@ -6,6 +6,9 @@ import type React from 'react';
6
6
  import type { SettingsConfig, CustomSettingsSection } from "../screens/types";
7
7
  import type { DevSettingsProps } from "../../domains/dev";
8
8
  import type { FAQCategory } from "../../domains/faqs";
9
+ import type { DisclaimerScreenParams } from "../../domains/disclaimer/presentation/screens/DisclaimerScreen";
10
+ import type { FeedbackScreenParams } from "../../domains/feedback/presentation/screens/FeedbackScreen";
11
+ import type { RatingPromptScreenParams } from "../../domains/rating/presentation/screens/RatingPromptScreen";
9
12
 
10
13
  /**
11
14
  * App Info passed from main app (APP_INFO constant)
@@ -48,6 +51,9 @@ export type SettingsStackParamList = {
48
51
  Account: undefined;
49
52
  VideoTutorial: undefined;
50
53
  FeatureRequest: undefined;
54
+ Disclaimer: DisclaimerScreenParams;
55
+ Feedback: FeedbackScreenParams;
56
+ RatingPrompt: RatingPromptScreenParams;
51
57
  PasswordPrompt: {
52
58
  onComplete: (password: string | null) => void;
53
59
  title?: string;
@@ -1,72 +0,0 @@
1
- /**
2
- * useDisclaimerModal Hook
3
- *
4
- * Refactored to use GenericModal and ModalConfig.
5
- * Replaces old DisclaimerModal component.
6
- *
7
- * @example
8
- * ```ts
9
- * const { DisclaimerModal, showDisclaimer } = useDisclaimerModal();
10
- *
11
- * return (
12
- * <>
13
- * <Button onPress={() => showDisclaimer({ title: 'Terms', content: '...' })} />
14
- * <DisclaimerModal />
15
- * </>
16
- * );
17
- * ```
18
- */
19
-
20
- import { useCallback } from 'react';
21
- import { GenericModal } from '../../../../presentation/components/GenericModal';
22
- import { useModalState } from '../../../../core/patterns/Modal/useModalState';
23
- import type { ModalConfig } from '../../../../core/patterns/Modal/ModalConfig';
24
-
25
- export interface DisclaimerModalOptions {
26
- /**
27
- * Disclaimer title
28
- */
29
- title: string;
30
-
31
- /**
32
- * Disclaimer content
33
- */
34
- content: string;
35
-
36
- /**
37
- * Custom modal configuration
38
- */
39
- config?: Partial<ModalConfig>;
40
- }
41
-
42
- /**
43
- * Disclaimer modal hook
44
- */
45
- export function useDisclaimerModal() {
46
- const modal = useModalState();
47
-
48
- const showDisclaimer = useCallback((options: DisclaimerModalOptions) => {
49
- const { title, content, config: customConfig } = options;
50
-
51
- const modalConfig: ModalConfig = {
52
- title,
53
- message: content,
54
- dismissible: true,
55
- closeOnBackdropPress: true,
56
- closeOnBackPress: true,
57
- ...customConfig,
58
- };
59
-
60
- modal.show(modalConfig);
61
- }, [modal]);
62
-
63
- const DisclaimerModal = useCallback(() => {
64
- return <GenericModal state={modal} testID="disclaimer-modal" />;
65
- }, [modal]);
66
-
67
- return {
68
- DisclaimerModal,
69
- showDisclaimer,
70
- modal,
71
- };
72
- }