@umituz/react-native-settings 4.23.32 → 4.23.34

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-settings",
3
- "version": "4.23.32",
3
+ "version": "4.23.34",
4
4
  "description": "Complete settings hub for React Native apps - consolidated package with settings, about, legal, appearance, feedback, FAQs, rating, and gamification",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -46,6 +46,7 @@
46
46
  "expo-notifications": ">=0.28.0",
47
47
  "expo-device": ">=6.0.0",
48
48
  "expo-haptics": ">=15.0.0",
49
+ "expo-store-review": ">=14.0.0",
49
50
  "@react-native-community/datetimepicker": ">=8.0.0",
50
51
  "react": ">=19.0.0",
51
52
  "react-native": ">=0.81.0",
@@ -91,6 +92,7 @@
91
92
  "expo-sharing": "^14.0.8",
92
93
  "expo-notifications": "~0.27.6",
93
94
  "expo-secure-store": "^15.0.8",
95
+ "expo-store-review": "~14.0.0",
94
96
  "expo-video": "^3.0.15",
95
97
  "expo-web-browser": "^12.0.0",
96
98
  "firebase": "^12.7.0",
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Rating Service
3
+ * Core business logic for app rating system
4
+ */
5
+
6
+ import type { RatingConfig, RatingState } from "../../domain/entities/RatingConfig";
7
+ import {
8
+ getEventCount,
9
+ incrementEventCount,
10
+ getLastPromptDate,
11
+ setLastPromptDate,
12
+ getHasRated,
13
+ setHasRated,
14
+ getDismissed,
15
+ setDismissed,
16
+ getRatingState,
17
+ reset as resetStorage,
18
+ } from "../../infrastructure/storage/RatingStorage";
19
+
20
+ /**
21
+ * Calculate days between two dates
22
+ */
23
+ function daysBetween(dateString: string, now: Date): number {
24
+ const date = new Date(dateString);
25
+ const diffMs = now.getTime() - date.getTime();
26
+ return Math.floor(diffMs / (1000 * 60 * 60 * 24));
27
+ }
28
+
29
+ /**
30
+ * Track an event occurrence
31
+ */
32
+ export async function trackEvent(eventType: string): Promise<void> {
33
+ try {
34
+ await incrementEventCount(eventType);
35
+
36
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
37
+ const count = await getEventCount(eventType);
38
+ console.log(`[RatingService] Event tracked: ${eventType}, count: ${count}`);
39
+ }
40
+ } catch (error) {
41
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
42
+ console.error("[RatingService] Error tracking event:", error);
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Check if prompt should be shown based on criteria
49
+ */
50
+ export async function shouldShowPrompt(config: RatingConfig): Promise<boolean> {
51
+ try {
52
+ const hasRated = await getHasRated();
53
+ if (hasRated) {
54
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
55
+ console.log("[RatingService] User has already rated, skipping prompt");
56
+ }
57
+ return false;
58
+ }
59
+
60
+ const dismissed = await getDismissed();
61
+ if (dismissed) {
62
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
63
+ console.log("[RatingService] User permanently dismissed prompt");
64
+ }
65
+ return false;
66
+ }
67
+
68
+ const eventCount = await getEventCount(config.eventType);
69
+ const minCount = config.minEventCount ?? 3;
70
+
71
+ if (eventCount < minCount) {
72
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
73
+ console.log(
74
+ `[RatingService] Event count ${eventCount} < ${minCount}, not showing prompt`
75
+ );
76
+ }
77
+ return false;
78
+ }
79
+
80
+ const lastPromptDate = await getLastPromptDate(config.eventType);
81
+
82
+ if (lastPromptDate) {
83
+ const cooldownDays = config.cooldownDays ?? 90;
84
+ const daysSinceLastPrompt = daysBetween(lastPromptDate, new Date());
85
+
86
+ if (daysSinceLastPrompt < cooldownDays) {
87
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
88
+ console.log(
89
+ `[RatingService] Cooldown period active: ${daysSinceLastPrompt}/${cooldownDays} days`
90
+ );
91
+ }
92
+ return false;
93
+ }
94
+ }
95
+
96
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
97
+ console.log("[RatingService] All criteria met, prompt should be shown");
98
+ }
99
+
100
+ return true;
101
+ } catch (error) {
102
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
103
+ console.error("[RatingService] Error checking criteria:", error);
104
+ }
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Mark that prompt was shown to user
111
+ */
112
+ export async function markPromptShown(eventType: string): Promise<void> {
113
+ try {
114
+ const now = new Date().toISOString();
115
+ await setLastPromptDate(eventType, now);
116
+
117
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
118
+ console.log(`[RatingService] Prompt shown marked for: ${eventType}`);
119
+ }
120
+ } catch (error) {
121
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
122
+ console.error("[RatingService] Error marking prompt shown:", error);
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Mark that user has rated the app
129
+ */
130
+ export async function markRated(): Promise<void> {
131
+ try {
132
+ await setHasRated(true);
133
+
134
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
135
+ console.log("[RatingService] User marked as rated");
136
+ }
137
+ } catch (error) {
138
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
139
+ console.error("[RatingService] Error marking rated:", error);
140
+ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Mark that user permanently dismissed the prompt
146
+ */
147
+ export async function markDismissed(): Promise<void> {
148
+ try {
149
+ await setDismissed(true);
150
+
151
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
152
+ console.log("[RatingService] User marked as dismissed");
153
+ }
154
+ } catch (error) {
155
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
156
+ console.error("[RatingService] Error marking dismissed:", error);
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Get current rating state for event type
163
+ */
164
+ export async function getState(eventType: string): Promise<RatingState> {
165
+ return getRatingState(eventType);
166
+ }
167
+
168
+ /**
169
+ * Reset rating data (for testing or specific event type)
170
+ */
171
+ export async function reset(eventType?: string): Promise<void> {
172
+ try {
173
+ await resetStorage(eventType);
174
+
175
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
176
+ console.log(
177
+ `[RatingService] Reset ${eventType ? `event: ${eventType}` : "all rating data"}`
178
+ );
179
+ }
180
+ } catch (error) {
181
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
182
+ console.error("[RatingService] Error resetting:", error);
183
+ }
184
+ }
185
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Rating Configuration
3
+ * Types and interfaces for app rating system
4
+ */
5
+
6
+ import type React from "react";
7
+
8
+ /**
9
+ * Rating prompt configuration
10
+ */
11
+ export interface RatingConfig {
12
+ /**
13
+ * Type of event to track (e.g., "ai_generation", "onboarding_completed")
14
+ */
15
+ eventType: string;
16
+
17
+ /**
18
+ * Minimum number of events before showing prompt
19
+ * @default 3
20
+ */
21
+ minEventCount?: number;
22
+
23
+ /**
24
+ * Cooldown period in days before showing prompt again
25
+ * @default 90
26
+ */
27
+ cooldownDays?: number;
28
+
29
+ /**
30
+ * App name to display in prompt
31
+ */
32
+ appName?: string;
33
+
34
+ /**
35
+ * Custom translations for the prompt
36
+ */
37
+ translations?: RatingTranslations;
38
+
39
+ /**
40
+ * Callback when user gives positive feedback
41
+ */
42
+ onPositiveFeedback?: () => void | Promise<void>;
43
+
44
+ /**
45
+ * Callback when user gives negative feedback
46
+ */
47
+ onNegativeFeedback?: () => void | Promise<void>;
48
+
49
+ /**
50
+ * Callback after prompt is shown
51
+ */
52
+ onPromptShown?: () => void | Promise<void>;
53
+
54
+ /**
55
+ * Callback when prompt is dismissed
56
+ */
57
+ onPromptDismissed?: () => void | Promise<void>;
58
+ }
59
+
60
+ /**
61
+ * Custom translations for rating prompt
62
+ */
63
+ export interface RatingTranslations {
64
+ title: string;
65
+ message: string;
66
+ positiveButton: string;
67
+ negativeButton: string;
68
+ laterButton?: string;
69
+ }
70
+
71
+ /**
72
+ * Rating state stored in AsyncStorage
73
+ */
74
+ export interface RatingState {
75
+ /**
76
+ * Event count for specific event type
77
+ */
78
+ eventCount: number;
79
+
80
+ /**
81
+ * Last time prompt was shown (ISO date string)
82
+ */
83
+ lastPromptDate: string | null;
84
+
85
+ /**
86
+ * Whether user has rated the app
87
+ */
88
+ hasRated: boolean;
89
+
90
+ /**
91
+ * Whether user permanently dismissed the prompt
92
+ */
93
+ dismissed: boolean;
94
+ }
95
+
96
+ /**
97
+ * Result of useAppRating hook
98
+ */
99
+ export interface UseAppRatingResult {
100
+ /**
101
+ * Track an event (e.g., generation completed)
102
+ */
103
+ trackEvent: () => Promise<void>;
104
+
105
+ /**
106
+ * Check if should show prompt and show it automatically
107
+ */
108
+ checkAndShow: () => Promise<boolean>;
109
+
110
+ /**
111
+ * Manually check if criteria is met (without showing)
112
+ */
113
+ shouldShow: () => Promise<boolean>;
114
+
115
+ /**
116
+ * Manually show the rating prompt
117
+ */
118
+ showPrompt: () => Promise<void>;
119
+
120
+ /**
121
+ * Reset all rating data (for testing)
122
+ */
123
+ reset: () => Promise<void>;
124
+
125
+ /**
126
+ * Get current rating state
127
+ */
128
+ getState: () => Promise<RatingState>;
129
+
130
+ /**
131
+ * Whether prompt is currently visible
132
+ */
133
+ isVisible: boolean;
134
+
135
+ /**
136
+ * Modal component to render in your screen
137
+ */
138
+ modal: React.ReactNode;
139
+ }
140
+
141
+ /**
142
+ * Default rating configuration
143
+ */
144
+ export const DEFAULT_RATING_CONFIG: Required<
145
+ Omit<RatingConfig, "onPositiveFeedback" | "onNegativeFeedback" | "onPromptShown" | "onPromptDismissed">
146
+ > = {
147
+ eventType: "app_usage",
148
+ minEventCount: 3,
149
+ cooldownDays: 90,
150
+ appName: "this app",
151
+ translations: {
152
+ title: "Enjoying the app?",
153
+ message: "If you love using our app, would you mind taking a moment to rate it?",
154
+ positiveButton: "Yes, I love it!",
155
+ negativeButton: "Not really",
156
+ laterButton: "Maybe later",
157
+ },
158
+ };
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Rating Domain
3
- * Star ratings, user reviews, and statistics
3
+ * Star ratings, user reviews, statistics, and app store rating prompts
4
4
  */
5
5
 
6
6
  // =============================================================================
7
- // DOMAIN LAYER - Entities
7
+ // DOMAIN LAYER - Entities (Star Ratings)
8
8
  // =============================================================================
9
9
 
10
10
  export type {
@@ -14,10 +14,36 @@ export type {
14
14
  } from './domain/entities/Rating';
15
15
 
16
16
  // =============================================================================
17
- // PRESENTATION LAYER - Components
17
+ // DOMAIN LAYER - Entities (App Store Rating)
18
+ // =============================================================================
19
+
20
+ export type {
21
+ RatingConfig,
22
+ RatingState,
23
+ RatingTranslations,
24
+ UseAppRatingResult,
25
+ } from './domain/entities/RatingConfig';
26
+
27
+ export { DEFAULT_RATING_CONFIG } from './domain/entities/RatingConfig';
28
+
29
+ // =============================================================================
30
+ // PRESENTATION LAYER - Components (Star Ratings)
18
31
  // =============================================================================
19
32
 
20
33
  export { StarRating } from './presentation/components/StarRating';
21
34
  export type { StarRatingProps } from './presentation/components/StarRating';
22
35
 
36
+ // =============================================================================
37
+ // PRESENTATION LAYER - Components (App Store Rating)
38
+ // =============================================================================
39
+
40
+ export { RatingPromptModal } from './presentation/components/RatingPromptModal';
41
+ export type { RatingPromptModalProps } from './presentation/components/RatingPromptModal';
42
+
43
+ // =============================================================================
44
+ // PRESENTATION LAYER - Hooks (App Store Rating)
45
+ // =============================================================================
46
+
47
+ export { useAppRating } from './presentation/hooks/useAppRating';
48
+
23
49
 
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Rating Storage Repository
3
+ * Storage layer for rating system using design system storageRepository
4
+ */
5
+
6
+ import { storageRepository, unwrap } from "@umituz/react-native-design-system";
7
+ import type { RatingState } from "../../domain/entities/RatingConfig";
8
+
9
+ /**
10
+ * Storage key generator
11
+ */
12
+ const KEYS = {
13
+ eventCount: (eventType: string) => `rating.${eventType}.count`,
14
+ lastPrompt: (eventType: string) => `rating.${eventType}.lastPrompt`,
15
+ hasRated: "rating.hasRated",
16
+ dismissed: "rating.dismissed",
17
+ } as const;
18
+
19
+ /**
20
+ * Get event count for specific event type
21
+ */
22
+ export async function getEventCount(eventType: string): Promise<number> {
23
+ try {
24
+ const result = await storageRepository.getString(KEYS.eventCount(eventType), "0");
25
+ const count = parseInt(unwrap(result, "0"), 10);
26
+ return Number.isNaN(count) ? 0 : count;
27
+ } catch (error) {
28
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
29
+ console.error("[RatingStorage] Error getting event count:", error);
30
+ }
31
+ return 0;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Set event count for specific event type
37
+ */
38
+ export async function setEventCount(eventType: string, count: number): Promise<void> {
39
+ try {
40
+ await storageRepository.setString(KEYS.eventCount(eventType), count.toString());
41
+ } catch (error) {
42
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
43
+ console.error("[RatingStorage] Error setting event count:", error);
44
+ }
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Increment event count for specific event type
50
+ */
51
+ export async function incrementEventCount(eventType: string): Promise<void> {
52
+ const currentCount = await getEventCount(eventType);
53
+ await setEventCount(eventType, currentCount + 1);
54
+ }
55
+
56
+ /**
57
+ * Get last prompt date for specific event type
58
+ */
59
+ export async function getLastPromptDate(eventType: string): Promise<string | null> {
60
+ try {
61
+ const result = await storageRepository.getString(KEYS.lastPrompt(eventType), "");
62
+ const date = unwrap(result, "");
63
+ return date || null;
64
+ } catch (error) {
65
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
66
+ console.error("[RatingStorage] Error getting last prompt date:", error);
67
+ }
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Set last prompt date for specific event type
74
+ */
75
+ export async function setLastPromptDate(eventType: string, date: string): Promise<void> {
76
+ try {
77
+ await storageRepository.setString(KEYS.lastPrompt(eventType), date);
78
+ } catch (error) {
79
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
80
+ console.error("[RatingStorage] Error setting last prompt date:", error);
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Check if user has rated the app
87
+ */
88
+ export async function getHasRated(): Promise<boolean> {
89
+ try {
90
+ const result = await storageRepository.getString(KEYS.hasRated, "false");
91
+ return unwrap(result, "false") === "true";
92
+ } catch (error) {
93
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
94
+ console.error("[RatingStorage] Error getting hasRated:", error);
95
+ }
96
+ return false;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Set whether user has rated the app
102
+ */
103
+ export async function setHasRated(value: boolean): Promise<void> {
104
+ try {
105
+ await storageRepository.setString(KEYS.hasRated, value.toString());
106
+ } catch (error) {
107
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
108
+ console.error("[RatingStorage] Error setting hasRated:", error);
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Check if user permanently dismissed the prompt
115
+ */
116
+ export async function getDismissed(): Promise<boolean> {
117
+ try {
118
+ const result = await storageRepository.getString(KEYS.dismissed, "false");
119
+ return unwrap(result, "false") === "true";
120
+ } catch (error) {
121
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
122
+ console.error("[RatingStorage] Error getting dismissed:", error);
123
+ }
124
+ return false;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Set whether user permanently dismissed the prompt
130
+ */
131
+ export async function setDismissed(value: boolean): Promise<void> {
132
+ try {
133
+ await storageRepository.setString(KEYS.dismissed, value.toString());
134
+ } catch (error) {
135
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
136
+ console.error("[RatingStorage] Error setting dismissed:", error);
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Get complete rating state for specific event type
143
+ */
144
+ export async function getRatingState(eventType: string): Promise<RatingState> {
145
+ const [eventCount, lastPromptDate, hasRated, dismissed] = await Promise.all([
146
+ getEventCount(eventType),
147
+ getLastPromptDate(eventType),
148
+ getHasRated(),
149
+ getDismissed(),
150
+ ]);
151
+
152
+ return {
153
+ eventCount,
154
+ lastPromptDate,
155
+ hasRated,
156
+ dismissed,
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Reset rating data for specific event type or all data
162
+ */
163
+ export async function reset(eventType?: string): Promise<void> {
164
+ try {
165
+ if (eventType) {
166
+ await storageRepository.removeItem(KEYS.eventCount(eventType));
167
+ await storageRepository.removeItem(KEYS.lastPrompt(eventType));
168
+ } else {
169
+ const allKeysResult = await storageRepository.getAllKeys();
170
+ const allKeys = unwrap(allKeysResult, []);
171
+ const ratingKeys = allKeys.filter((key) => key.startsWith("rating."));
172
+
173
+ await Promise.all(
174
+ ratingKeys.map((key) => storageRepository.removeItem(key))
175
+ );
176
+ }
177
+ } catch (error) {
178
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
179
+ console.error("[RatingStorage] Error resetting data:", error);
180
+ }
181
+ }
182
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Rating Prompt Modal
3
+ * 2-step rating prompt: Custom modal → Native review prompt
4
+ */
5
+
6
+ import React from "react";
7
+ import { Modal, View, StyleSheet, type StyleProp, type ViewStyle } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ AtomicButton,
11
+ AtomicIcon,
12
+ useAppDesignTokens,
13
+ useResponsive,
14
+ } from "@umituz/react-native-design-system";
15
+ import type { RatingTranslations } from "../../domain/entities/RatingConfig";
16
+
17
+ export interface RatingPromptModalProps {
18
+ visible: boolean;
19
+ onPositive: () => void;
20
+ onNegative: () => void;
21
+ onLater: () => void;
22
+ onDismiss: () => void;
23
+ translations?: RatingTranslations;
24
+ appName?: string;
25
+ }
26
+
27
+ export const RatingPromptModal: React.FC<RatingPromptModalProps> = ({
28
+ visible,
29
+ onPositive,
30
+ onNegative,
31
+ onLater,
32
+ onDismiss,
33
+ translations,
34
+ appName = "this app",
35
+ }) => {
36
+ const tokens = useAppDesignTokens();
37
+ const responsive = useResponsive();
38
+
39
+ const defaultTranslations: RatingTranslations = {
40
+ title: translations?.title ?? "Enjoying the app?",
41
+ message:
42
+ translations?.message ??
43
+ `If you love using ${appName}, would you mind taking a moment to rate it?`,
44
+ positiveButton: translations?.positiveButton ?? "Yes, I love it!",
45
+ negativeButton: translations?.negativeButton ?? "Not really",
46
+ laterButton: translations?.laterButton ?? "Maybe later",
47
+ };
48
+
49
+ return (
50
+ <Modal
51
+ visible={visible}
52
+ transparent
53
+ animationType="none"
54
+ onRequestClose={onDismiss}
55
+ statusBarTranslucent
56
+ >
57
+ <View
58
+ style={[
59
+ styles.overlay,
60
+ { backgroundColor: "rgba(0, 0, 0, 0.5)" },
61
+ ]}
62
+ >
63
+ <View
64
+ style={[
65
+ styles.container,
66
+ {
67
+ backgroundColor: tokens.colors.surface,
68
+ borderRadius: tokens.borders.radius.xl,
69
+ padding: tokens.spacing.lg,
70
+ maxWidth: responsive.maxContentWidth * 0.85,
71
+ width: "90%",
72
+ },
73
+ ]}
74
+ >
75
+ <View style={styles.iconContainer}>
76
+ <AtomicIcon name="star" size="xl" color="primary" />
77
+ </View>
78
+
79
+ <AtomicText
80
+ type="headlineMedium"
81
+ color="onSurface"
82
+ style={[
83
+ styles.title,
84
+ { marginBottom: tokens.spacing.sm },
85
+ ]}
86
+ >
87
+ {defaultTranslations.title}
88
+ </AtomicText>
89
+
90
+ <AtomicText
91
+ type="bodyMedium"
92
+ color="onSurfaceVariant"
93
+ style={[
94
+ styles.message,
95
+ { marginBottom: tokens.spacing.lg },
96
+ ]}
97
+ >
98
+ {defaultTranslations.message}
99
+ </AtomicText>
100
+
101
+ <View style={[styles.buttonContainer, { gap: tokens.spacing.sm }]}>
102
+ <AtomicButton
103
+ variant="fill"
104
+ onPress={onPositive}
105
+ style={styles.button}
106
+ >
107
+ {defaultTranslations.positiveButton}
108
+ </AtomicButton>
109
+
110
+ <AtomicButton
111
+ variant="outline"
112
+ onPress={onNegative}
113
+ style={styles.button}
114
+ >
115
+ {defaultTranslations.negativeButton}
116
+ </AtomicButton>
117
+
118
+ <AtomicButton
119
+ variant="text"
120
+ onPress={onLater}
121
+ style={styles.button}
122
+ >
123
+ {defaultTranslations.laterButton}
124
+ </AtomicButton>
125
+ </View>
126
+ </View>
127
+ </View>
128
+ </Modal>
129
+ );
130
+ };
131
+
132
+ const styles = StyleSheet.create({
133
+ overlay: {
134
+ flex: 1,
135
+ justifyContent: "center",
136
+ alignItems: "center",
137
+ },
138
+ container: {
139
+ alignItems: "center",
140
+ },
141
+ iconContainer: {
142
+ marginBottom: 16,
143
+ },
144
+ title: {
145
+ textAlign: "center",
146
+ },
147
+ message: {
148
+ textAlign: "center",
149
+ },
150
+ buttonContainer: {
151
+ width: "100%",
152
+ },
153
+ button: {
154
+ width: "100%",
155
+ },
156
+ });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * useAppRating Hook
3
+ * React hook for app rating system
4
+ */
5
+
6
+ import React, { useState, useCallback } from "react";
7
+ import * as StoreReview from "expo-store-review";
8
+ import type {
9
+ RatingConfig,
10
+ UseAppRatingResult,
11
+ RatingState,
12
+ } from "../../domain/entities/RatingConfig";
13
+ import { DEFAULT_RATING_CONFIG } from "../../domain/entities/RatingConfig";
14
+ import * as RatingService from "../../application/services/RatingService";
15
+ import { RatingPromptModal } from "../components/RatingPromptModal";
16
+
17
+ /**
18
+ * App rating hook with 2-step prompt flow
19
+ */
20
+ export function useAppRating(config: RatingConfig): UseAppRatingResult {
21
+ const [isVisible, setIsVisible] = useState(false);
22
+
23
+ const mergedConfig: RatingConfig = {
24
+ ...DEFAULT_RATING_CONFIG,
25
+ ...config,
26
+ };
27
+
28
+ const trackEvent = useCallback(async (): Promise<void> => {
29
+ await RatingService.trackEvent(mergedConfig.eventType);
30
+ }, [mergedConfig.eventType]);
31
+
32
+ const shouldShow = useCallback(async (): Promise<boolean> => {
33
+ return RatingService.shouldShowPrompt(mergedConfig);
34
+ }, [mergedConfig]);
35
+
36
+ const showPrompt = useCallback(async (): Promise<void> => {
37
+ setIsVisible(true);
38
+ await RatingService.markPromptShown(mergedConfig.eventType);
39
+
40
+ if (mergedConfig.onPromptShown) {
41
+ await mergedConfig.onPromptShown();
42
+ }
43
+ }, [mergedConfig]);
44
+
45
+ const checkAndShow = useCallback(async (): Promise<boolean> => {
46
+ const should = await shouldShow();
47
+
48
+ if (should) {
49
+ await showPrompt();
50
+ return true;
51
+ }
52
+
53
+ return false;
54
+ }, [shouldShow, showPrompt]);
55
+
56
+ const reset = useCallback(async (): Promise<void> => {
57
+ await RatingService.reset(mergedConfig.eventType);
58
+ }, [mergedConfig.eventType]);
59
+
60
+ const getState = useCallback(async (): Promise<RatingState> => {
61
+ return RatingService.getState(mergedConfig.eventType);
62
+ }, [mergedConfig.eventType]);
63
+
64
+ const handlePositive = useCallback(async () => {
65
+ setIsVisible(false);
66
+ await RatingService.markRated();
67
+
68
+ try {
69
+ const isAvailable = await StoreReview.isAvailableAsync();
70
+
71
+ if (isAvailable) {
72
+ await StoreReview.requestReview();
73
+ }
74
+ } catch (error) {
75
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
76
+ console.error("[useAppRating] Error requesting review:", error);
77
+ }
78
+ }
79
+
80
+ if (mergedConfig.onPositiveFeedback) {
81
+ await mergedConfig.onPositiveFeedback();
82
+ }
83
+ }, [mergedConfig]);
84
+
85
+ const handleNegative = useCallback(async () => {
86
+ setIsVisible(false);
87
+
88
+ if (mergedConfig.onNegativeFeedback) {
89
+ await mergedConfig.onNegativeFeedback();
90
+ }
91
+ }, [mergedConfig]);
92
+
93
+ const handleLater = useCallback(() => {
94
+ setIsVisible(false);
95
+ }, []);
96
+
97
+ const handleDismiss = useCallback(async () => {
98
+ setIsVisible(false);
99
+ await RatingService.markDismissed();
100
+
101
+ if (mergedConfig.onPromptDismissed) {
102
+ await mergedConfig.onPromptDismissed();
103
+ }
104
+ }, [mergedConfig]);
105
+
106
+ const modal = (
107
+ <RatingPromptModal
108
+ visible={isVisible}
109
+ onPositive={handlePositive}
110
+ onNegative={handleNegative}
111
+ onLater={handleLater}
112
+ onDismiss={handleDismiss}
113
+ translations={mergedConfig.translations}
114
+ appName={mergedConfig.appName}
115
+ />
116
+ );
117
+
118
+ return {
119
+ trackEvent,
120
+ checkAndShow,
121
+ shouldShow,
122
+ showPrompt,
123
+ reset,
124
+ getState,
125
+ isVisible,
126
+ modal,
127
+ } as UseAppRatingResult & { modal: React.ReactNode };
128
+ }
package/src/index.ts CHANGED
@@ -109,8 +109,22 @@ export * from './domains/feedback';
109
109
  // FAQs Domain - Frequently asked questions
110
110
  export * from './domains/faqs';
111
111
 
112
- // Rating Domain - Star ratings, reviews, statistics
113
- export * from "./domains/rating";
112
+ // Rating Domain - Star ratings, reviews, statistics, app store rating
113
+ export {
114
+ StarRating,
115
+ RatingPromptModal,
116
+ useAppRating,
117
+ DEFAULT_RATING_CONFIG,
118
+ type RatingValue,
119
+ type Rating,
120
+ type RatingStats,
121
+ type RatingConfig as AppStoreRatingConfig,
122
+ type RatingState as AppStoreRatingState,
123
+ type RatingTranslations as AppStoreRatingTranslations,
124
+ type UseAppRatingResult,
125
+ type StarRatingProps,
126
+ type RatingPromptModalProps,
127
+ } from "./domains/rating";
114
128
 
115
129
  // Video Tutorials Domain - Learning resources, tutorials
116
130
  export * from "./domains/video-tutorials";