@umituz/react-native-settings 5.4.9 → 5.4.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/package.json +1 -1
- package/src/core/base/BaseService.ts +141 -0
- package/src/core/index.ts +60 -0
- package/src/core/patterns/Modal/ModalConfig.ts +282 -0
- package/src/core/patterns/Modal/useModalState.ts +128 -0
- package/src/core/patterns/Screen/ScreenConfig.ts +375 -0
- package/src/core/patterns/Screen/useScreenData.ts +201 -0
- package/src/core/utils/logger.ts +138 -0
- package/src/core/utils/validators.ts +203 -0
- package/src/domains/disclaimer/index.ts +0 -3
- package/src/domains/disclaimer/presentation/components/DisclaimerSetting.tsx +18 -43
- package/src/domains/disclaimer/presentation/screens/DisclaimerScreen.tsx +42 -92
- package/src/domains/feedback/index.ts +2 -1
- package/src/domains/feedback/presentation/components/SupportSection.tsx +16 -43
- package/src/domains/feedback/presentation/screens/FeatureRequestScreen.tsx +4 -4
- package/src/domains/feedback/presentation/screens/FeedbackScreen.tsx +75 -0
- package/src/domains/notifications/infrastructure/services/NotificationService.ts +16 -13
- package/src/domains/rating/application/services/RatingService.ts +115 -79
- package/src/domains/rating/index.ts +3 -3
- package/src/domains/rating/presentation/hooks/useAppRating.tsx +42 -65
- package/src/domains/rating/presentation/screens/RatingPromptScreen.tsx +162 -0
- package/src/index.ts +12 -0
- package/src/infrastructure/services/SettingsService.ts +23 -19
- package/src/presentation/components/GenericModal.tsx +208 -0
- package/src/presentation/components/GenericScreen.tsx +273 -0
- package/src/presentation/components/index.ts +27 -0
- package/src/presentation/navigation/hooks/useSettingsScreens.ts +26 -1
- package/src/presentation/navigation/types.ts +6 -0
- package/src/domains/disclaimer/presentation/components/DisclaimerModal.tsx +0 -103
- package/src/domains/feedback/presentation/components/FeedbackModal.tsx +0 -99
- package/src/domains/rating/presentation/components/RatingPromptModal.tsx +0 -152
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Notification Service
|
|
3
3
|
*
|
|
4
4
|
* Simple facade for offline notification system.
|
|
5
5
|
* Works in ALL apps - offline, online, hybrid - no backend required.
|
|
6
|
+
* Refactored to extend BaseService.
|
|
6
7
|
*
|
|
7
8
|
* @module NotificationService
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { NotificationManager } from './NotificationManager';
|
|
11
|
-
import {
|
|
12
|
+
import { BaseService } from '../../../../core/base/BaseService';
|
|
12
13
|
|
|
13
14
|
export * from './types';
|
|
14
15
|
|
|
@@ -16,26 +17,22 @@ export * from './types';
|
|
|
16
17
|
* Notification service singleton
|
|
17
18
|
* Provides simple access to notification manager
|
|
18
19
|
*/
|
|
19
|
-
export class NotificationService {
|
|
20
|
+
export class NotificationService extends BaseService {
|
|
21
|
+
protected serviceName = 'NotificationService';
|
|
20
22
|
private static instance: NotificationService | null = null;
|
|
21
23
|
private isConfigured = false;
|
|
22
24
|
|
|
23
25
|
readonly notifications = new NotificationManager();
|
|
24
26
|
|
|
25
27
|
private constructor() {
|
|
28
|
+
super();
|
|
26
29
|
// Configuration deferred to first use
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
private ensureConfigured() {
|
|
30
33
|
if (!this.isConfigured) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
this.isConfigured = true;
|
|
34
|
-
} catch (error) {
|
|
35
|
-
if (isDev()) {
|
|
36
|
-
console.error('[NotificationService] Failed to configure NotificationManager:', error);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
34
|
+
NotificationManager.configure();
|
|
35
|
+
this.isConfigured = true;
|
|
39
36
|
}
|
|
40
37
|
}
|
|
41
38
|
|
|
@@ -51,7 +48,10 @@ export class NotificationService {
|
|
|
51
48
|
*/
|
|
52
49
|
async requestPermissions(): Promise<boolean> {
|
|
53
50
|
this.ensureConfigured();
|
|
54
|
-
|
|
51
|
+
const result = await this.execute('requestPermissions', async () => {
|
|
52
|
+
return await this.notifications.requestPermissions();
|
|
53
|
+
});
|
|
54
|
+
return result.success ? result.data : false;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -59,7 +59,10 @@ export class NotificationService {
|
|
|
59
59
|
*/
|
|
60
60
|
async hasPermissions(): Promise<boolean> {
|
|
61
61
|
this.ensureConfigured();
|
|
62
|
-
|
|
62
|
+
const result = await this.execute('hasPermissions', async () => {
|
|
63
|
+
return await this.notifications.hasPermissions();
|
|
64
|
+
});
|
|
65
|
+
return result.success ? result.data : false;
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rating Service
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Core business logic for app rating system.
|
|
5
|
+
* Refactored to extend BaseService for consistent error handling.
|
|
6
|
+
*
|
|
7
|
+
* @module RatingService
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
import type { RatingConfig, RatingState } from "../../domain/entities/RatingConfig";
|
|
7
|
-
import {
|
|
11
|
+
import { BaseService } from "../../../../core/base/BaseService";
|
|
8
12
|
import {
|
|
9
13
|
getEventCount,
|
|
10
14
|
incrementEventCount,
|
|
@@ -35,112 +39,144 @@ function toISOString(date: Date = new Date()): string {
|
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
|
-
*
|
|
42
|
+
* Rating Service Class
|
|
39
43
|
*/
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
export class RatingService extends BaseService {
|
|
45
|
+
protected serviceName = "RatingService";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Track an event occurrence
|
|
49
|
+
*/
|
|
50
|
+
async trackEvent(eventType: string): Promise<void> {
|
|
51
|
+
await this.execute("trackEvent", async () => {
|
|
52
|
+
await incrementEventCount(eventType);
|
|
53
|
+
});
|
|
47
54
|
}
|
|
48
|
-
}
|
|
49
55
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if prompt should be shown based on criteria
|
|
58
|
+
*/
|
|
59
|
+
async shouldShowPrompt(config: RatingConfig): Promise<boolean> {
|
|
60
|
+
const result = await this.execute("shouldShowPrompt", async () => {
|
|
61
|
+
const hasRated = await getHasRated();
|
|
62
|
+
if (hasRated) return false;
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
+
const dismissed = await getDismissed();
|
|
65
|
+
if (dismissed) return false;
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
const eventCount = await getEventCount(config.eventType);
|
|
68
|
+
const minCount = config.minEventCount ?? 3;
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
70
|
+
if (eventCount < minCount) return false;
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
const lastPromptDate = await getLastPromptDate(config.eventType);
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
if (lastPromptDate) {
|
|
75
|
+
const cooldownDays = config.cooldownDays ?? 90;
|
|
76
|
+
const daysSinceLastPrompt = daysBetween(lastPromptDate, new Date());
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
if (daysSinceLastPrompt < cooldownDays) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
80
81
|
}
|
|
81
|
-
}
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
return true;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return result.success ? result.data : false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Mark that prompt was shown to user
|
|
91
|
+
*/
|
|
92
|
+
async markPromptShown(eventType: string): Promise<void> {
|
|
93
|
+
await this.execute("markPromptShown", async () => {
|
|
94
|
+
await setLastPromptDate(eventType, toISOString());
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Mark that user has rated the app
|
|
100
|
+
*/
|
|
101
|
+
async markRated(): Promise<void> {
|
|
102
|
+
await this.execute("markRated", async () => {
|
|
103
|
+
await setHasRated(true);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Mark that user permanently dismissed the prompt
|
|
109
|
+
*/
|
|
110
|
+
async markDismissed(): Promise<void> {
|
|
111
|
+
await this.execute("markDismissed", async () => {
|
|
112
|
+
await setDismissed(true);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get current rating state for event type
|
|
118
|
+
*/
|
|
119
|
+
async getState(eventType: string): Promise<RatingState> {
|
|
120
|
+
return getRatingState(eventType);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Reset rating data (for testing or specific event type)
|
|
125
|
+
*/
|
|
126
|
+
async reset(eventType?: string): Promise<void> {
|
|
127
|
+
await this.execute("reset", async () => {
|
|
128
|
+
await resetStorage(eventType);
|
|
129
|
+
});
|
|
86
130
|
}
|
|
87
131
|
}
|
|
88
132
|
|
|
89
133
|
/**
|
|
90
|
-
*
|
|
134
|
+
* Singleton instance
|
|
91
135
|
*/
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
await setLastPromptDate(eventType, toISOString());
|
|
95
|
-
} catch (error) {
|
|
96
|
-
if (isDev()) {
|
|
97
|
-
console.error('[RatingService] Failed to mark prompt shown:', eventType, error);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
136
|
+
let ratingServiceInstance: RatingService | null = null;
|
|
101
137
|
|
|
102
138
|
/**
|
|
103
|
-
*
|
|
139
|
+
* Get rating service singleton instance
|
|
104
140
|
*/
|
|
105
|
-
export
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
} catch (error) {
|
|
109
|
-
if (isDev()) {
|
|
110
|
-
console.error('[RatingService] Failed to mark as rated:', error);
|
|
111
|
-
}
|
|
141
|
+
export function getRatingService(): RatingService {
|
|
142
|
+
if (!ratingServiceInstance) {
|
|
143
|
+
ratingServiceInstance = new RatingService();
|
|
112
144
|
}
|
|
145
|
+
return ratingServiceInstance;
|
|
113
146
|
}
|
|
114
147
|
|
|
115
148
|
/**
|
|
116
|
-
*
|
|
149
|
+
* Default export for backward compatibility
|
|
117
150
|
*/
|
|
118
|
-
export
|
|
119
|
-
try {
|
|
120
|
-
await setDismissed(true);
|
|
121
|
-
} catch (error) {
|
|
122
|
-
if (isDev()) {
|
|
123
|
-
console.error('[RatingService] Failed to mark as dismissed:', error);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
151
|
+
export const ratingService = getRatingService();
|
|
127
152
|
|
|
128
153
|
/**
|
|
129
|
-
*
|
|
154
|
+
* Static convenience functions that delegate to singleton
|
|
130
155
|
*/
|
|
156
|
+
export async function trackEvent(eventType: string): Promise<void> {
|
|
157
|
+
await ratingService.trackEvent(eventType);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function shouldShowPrompt(config: RatingConfig): Promise<boolean> {
|
|
161
|
+
return await ratingService.shouldShowPrompt(config);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function markPromptShown(eventType: string): Promise<void> {
|
|
165
|
+
await ratingService.markPromptShown(eventType);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function markRated(): Promise<void> {
|
|
169
|
+
await ratingService.markRated();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function markDismissed(): Promise<void> {
|
|
173
|
+
await ratingService.markDismissed();
|
|
174
|
+
}
|
|
175
|
+
|
|
131
176
|
export async function getState(eventType: string): Promise<RatingState> {
|
|
132
|
-
return
|
|
177
|
+
return await ratingService.getState(eventType);
|
|
133
178
|
}
|
|
134
179
|
|
|
135
|
-
/**
|
|
136
|
-
* Reset rating data (for testing or specific event type)
|
|
137
|
-
*/
|
|
138
180
|
export async function reset(eventType?: string): Promise<void> {
|
|
139
|
-
|
|
140
|
-
await resetStorage(eventType);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
if (isDev()) {
|
|
143
|
-
console.error('[RatingService] Failed to reset:', eventType, error);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
181
|
+
await ratingService.reset(eventType);
|
|
146
182
|
}
|
|
@@ -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 -
|
|
37
|
+
// PRESENTATION LAYER - Screens
|
|
38
38
|
// =============================================================================
|
|
39
39
|
|
|
40
|
-
export {
|
|
41
|
-
export type {
|
|
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, {
|
|
7
|
+
import React, { 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 {
|
|
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
|
|
19
|
+
* App rating hook with navigation-based prompt flow
|
|
20
20
|
*/
|
|
21
21
|
export function useAppRating(config: RatingConfig): UseAppRatingResult {
|
|
22
|
-
const
|
|
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
|
-
|
|
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
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,18 @@
|
|
|
8
8
|
* import { useSettings, SettingsScreen, AppearanceScreen, SettingsItemCard, DisclaimerSetting } from '@umituz/react-native-settings';
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// CORE LAYER - Base Classes & Patterns
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export * from './core';
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// PRESENTATION LAYER - Generic Components
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
export * from './presentation/components';
|
|
22
|
+
|
|
11
23
|
// =============================================================================
|
|
12
24
|
// DOMAIN LAYER - Repository Interfaces
|
|
13
25
|
// =============================================================================
|