@umituz/react-native-settings 5.4.8 → 5.4.10
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 -4
- 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 +202 -0
- package/src/core/utils/logger.ts +138 -0
- package/src/core/utils/validators.ts +203 -0
- package/src/domains/disclaimer/index.ts +2 -2
- package/src/domains/disclaimer/presentation/hooks/useDisclaimerModal.ts +72 -0
- package/src/domains/feedback/index.ts +2 -1
- package/src/domains/feedback/presentation/hooks/useFeedbackModal.ts +182 -0
- package/src/domains/notifications/infrastructure/services/NotificationService.ts +16 -11
- package/src/domains/rating/application/services/RatingService.ts +89 -84
- package/src/domains/rating/index.ts +2 -2
- package/src/domains/rating/presentation/hooks/useRatingPromptModal.ts +122 -0
- package/src/index.ts +12 -0
- package/src/infrastructure/services/SettingsService.ts +33 -23
- package/src/presentation/components/GenericModal.tsx +212 -0
- package/src/presentation/components/GenericScreen.tsx +278 -0
- package/src/presentation/components/index.ts +27 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-settings",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.10",
|
|
4
4
|
"description": "Complete settings hub for React Native apps - consolidated package with settings, localization, about, legal, appearance, feedback, FAQs, rating, and gamification - expo-store-review and expo-device now lazy loaded",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -153,9 +153,6 @@
|
|
|
153
153
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
154
154
|
"@react-native-community/datetimepicker": "^8.6.0",
|
|
155
155
|
"@react-native-community/slider": "^5.1.1",
|
|
156
|
-
"@react-navigation/bottom-tabs": "^7.9.0",
|
|
157
|
-
"@react-navigation/native": "^7.1.26",
|
|
158
|
-
"@react-navigation/stack": "^7.6.13",
|
|
159
156
|
"@tanstack/query-async-storage-persister": "^5.66.7",
|
|
160
157
|
"@tanstack/react-query": "^5.0.0",
|
|
161
158
|
"@tanstack/react-query-persist-client": "^5.66.7",
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseService
|
|
3
|
+
*
|
|
4
|
+
* Abstract base class for all domain services.
|
|
5
|
+
* Provides consistent error handling, logging, and result patterns.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* class MyService extends BaseService {
|
|
10
|
+
* protected serviceName = 'MyService';
|
|
11
|
+
*
|
|
12
|
+
* async doSomething(input: Input): Promise<Data> {
|
|
13
|
+
* return this.execute('doSomething', async () => {
|
|
14
|
+
* // Your logic here
|
|
15
|
+
* return result;
|
|
16
|
+
* });
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { isDev } from '../../utils/devUtils';
|
|
23
|
+
import { formatErrorMessage } from '../../utils/errorUtils';
|
|
24
|
+
|
|
25
|
+
export type Result<T> =
|
|
26
|
+
| { success: true; data: T }
|
|
27
|
+
| { success: false; error: string };
|
|
28
|
+
|
|
29
|
+
export type AsyncResult<T> = Promise<Result<T>>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Abstract base service with error handling and logging
|
|
33
|
+
*/
|
|
34
|
+
export abstract class BaseService {
|
|
35
|
+
/**
|
|
36
|
+
* Service name for logging (must be implemented by subclass)
|
|
37
|
+
*/
|
|
38
|
+
protected abstract serviceName: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Execute an operation with automatic error handling and logging
|
|
42
|
+
*
|
|
43
|
+
* @param operation - Operation name for logging
|
|
44
|
+
* @param fn - Async function to execute
|
|
45
|
+
* @returns Result object with success flag
|
|
46
|
+
*/
|
|
47
|
+
protected async execute<T>(
|
|
48
|
+
operation: string,
|
|
49
|
+
fn: () => Promise<T>
|
|
50
|
+
): AsyncResult<T> {
|
|
51
|
+
try {
|
|
52
|
+
const data = await fn();
|
|
53
|
+
return { success: true, data };
|
|
54
|
+
} catch (error) {
|
|
55
|
+
this.logError(operation, error);
|
|
56
|
+
return { success: false, error: formatErrorMessage(error) };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Execute without error handling (for critical operations that should crash)
|
|
62
|
+
*
|
|
63
|
+
* @param operation - Operation name for logging
|
|
64
|
+
* @param fn - Async function to execute
|
|
65
|
+
* @returns Direct result from function
|
|
66
|
+
*/
|
|
67
|
+
protected async executeUnsafe<T>(
|
|
68
|
+
operation: string,
|
|
69
|
+
fn: () => Promise<T>
|
|
70
|
+
): Promise<T> {
|
|
71
|
+
try {
|
|
72
|
+
return await fn();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
this.logError(operation, error);
|
|
75
|
+
throw error; // Re-throw for caller to handle
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Execute a synchronous operation with error handling
|
|
81
|
+
*
|
|
82
|
+
* @param operation - Operation name for logging
|
|
83
|
+
* @param fn - Sync function to execute
|
|
84
|
+
* @returns Result object with success flag
|
|
85
|
+
*/
|
|
86
|
+
protected executeSync<T>(
|
|
87
|
+
operation: string,
|
|
88
|
+
fn: () => T
|
|
89
|
+
): Result<T> {
|
|
90
|
+
try {
|
|
91
|
+
const data = fn();
|
|
92
|
+
return { success: true, data };
|
|
93
|
+
} catch (error) {
|
|
94
|
+
this.logError(operation, error);
|
|
95
|
+
return { success: false, error: formatErrorMessage(error) };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Log error in development mode
|
|
101
|
+
*
|
|
102
|
+
* @param operation - Operation name
|
|
103
|
+
* @param error - Error to log
|
|
104
|
+
*/
|
|
105
|
+
protected logError(operation: string, error: unknown): void {
|
|
106
|
+
if (isDev()) {
|
|
107
|
+
console.error(`[${this.serviceName}] ${operation}:`, error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Log info in development mode
|
|
113
|
+
*
|
|
114
|
+
* @param operation - Operation name
|
|
115
|
+
* @param message - Message to log
|
|
116
|
+
*/
|
|
117
|
+
protected logInfo(operation: string, message: string): void {
|
|
118
|
+
if (isDev()) {
|
|
119
|
+
console.log(`[${this.serviceName}] ${operation}:`, message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Log warning in development mode
|
|
125
|
+
*
|
|
126
|
+
* @param operation - Operation name
|
|
127
|
+
* @param message - Warning message
|
|
128
|
+
*/
|
|
129
|
+
protected logWarning(operation: string, message: string): void {
|
|
130
|
+
if (isDev()) {
|
|
131
|
+
console.warn(`[${this.serviceName}] ${operation}:`, message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if running in development mode
|
|
137
|
+
*/
|
|
138
|
+
protected get isDev(): boolean {
|
|
139
|
+
return isDev();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Layer - Public API
|
|
3
|
+
*
|
|
4
|
+
* Foundational utilities and base classes for the entire application.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { BaseService, logger, ModalPresets } from '@/core';
|
|
9
|
+
*
|
|
10
|
+
* class MyService extends BaseService {
|
|
11
|
+
* protected serviceName = 'MyService';
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// BASE CLASSES
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
export { BaseService } from './base/BaseService';
|
|
21
|
+
export type { Result, AsyncResult } from './base/BaseService';
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// UTILITIES
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
export { logger, log, createLogger } from './utils/logger';
|
|
28
|
+
export type { Logger, LogContext } from './utils/logger';
|
|
29
|
+
|
|
30
|
+
export { validators, valid, invalid, validateAll } from './utils/validators';
|
|
31
|
+
export type { ValidationResult } from './utils/validators';
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// PATTERNS - MODAL
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
export { useModalState, useModalStateWithResult } from './patterns/Modal/useModalState';
|
|
38
|
+
export type { ModalState, ModalConfig, ModalAction, ModalContent, ModalBehavior, ModalResult } from './patterns/Modal/ModalConfig';
|
|
39
|
+
export { ModalPresets, confirmed, dismissed } from './patterns/Modal/ModalConfig';
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// PATTERNS - SCREEN
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
export { useScreenData, useSimpleScreenData } from './patterns/Screen/useScreenData';
|
|
46
|
+
export type {
|
|
47
|
+
ScreenData,
|
|
48
|
+
ScreenDataState,
|
|
49
|
+
ScreenDataActions,
|
|
50
|
+
ScreenFetchFunction,
|
|
51
|
+
ScreenConfig,
|
|
52
|
+
ScreenHeader,
|
|
53
|
+
ScreenLoading,
|
|
54
|
+
ScreenError,
|
|
55
|
+
ScreenEmpty,
|
|
56
|
+
ScreenLayout,
|
|
57
|
+
UseScreenDataOptions,
|
|
58
|
+
} from './patterns/Screen/ScreenConfig';
|
|
59
|
+
|
|
60
|
+
export { ScreenPresets } from './patterns/Screen/ScreenConfig';
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal Configuration Types
|
|
3
|
+
*
|
|
4
|
+
* Standardized configuration for all modal components.
|
|
5
|
+
* Ensures consistency across the application.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const config: ModalConfig = {
|
|
10
|
+
* title: 'Rating',
|
|
11
|
+
* content: 'Rate this app',
|
|
12
|
+
* actions: [
|
|
13
|
+
* { label: 'OK', onPress: () => {}, variant: 'primary' }
|
|
14
|
+
* ]
|
|
15
|
+
* };
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ReactNode } from 'react';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Modal action button configuration
|
|
23
|
+
*/
|
|
24
|
+
export interface ModalAction {
|
|
25
|
+
/**
|
|
26
|
+
* Button label text
|
|
27
|
+
*/
|
|
28
|
+
label: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Button press handler
|
|
32
|
+
*/
|
|
33
|
+
onPress: () => void | Promise<void>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Button variant
|
|
37
|
+
*/
|
|
38
|
+
variant?: 'primary' | 'secondary' | 'outline' | 'text' | 'danger';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Disable button
|
|
42
|
+
*/
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Show loading indicator
|
|
47
|
+
*/
|
|
48
|
+
loading?: boolean;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Test ID for E2E testing
|
|
52
|
+
*/
|
|
53
|
+
testID?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Modal content configuration
|
|
58
|
+
*/
|
|
59
|
+
export interface ModalContent {
|
|
60
|
+
/**
|
|
61
|
+
* Modal title
|
|
62
|
+
*/
|
|
63
|
+
title?: string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Modal subtitle or description
|
|
67
|
+
*/
|
|
68
|
+
subtitle?: string;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Main content (can be string or React component)
|
|
72
|
+
*/
|
|
73
|
+
message?: string | ReactNode;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Custom icon name
|
|
77
|
+
*/
|
|
78
|
+
icon?: string;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Icon color
|
|
82
|
+
*/
|
|
83
|
+
iconColor?: string;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Custom header component (overrides title/subtitle)
|
|
87
|
+
*/
|
|
88
|
+
header?: ReactNode;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Custom body component (overrides message)
|
|
92
|
+
*/
|
|
93
|
+
body?: ReactNode;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Custom footer component (overrides actions)
|
|
97
|
+
*/
|
|
98
|
+
footer?: ReactNode;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Modal behavior configuration
|
|
103
|
+
*/
|
|
104
|
+
export interface ModalBehavior {
|
|
105
|
+
/**
|
|
106
|
+
* Close on backdrop press
|
|
107
|
+
*/
|
|
108
|
+
closeOnBackdropPress?: boolean;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Close on back button press (Android)
|
|
112
|
+
*/
|
|
113
|
+
closeOnBackPress?: boolean;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Animation type
|
|
117
|
+
*/
|
|
118
|
+
animation?: 'none' | 'slide' | 'fade' | 'scale';
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Dismissible flag
|
|
122
|
+
*/
|
|
123
|
+
dismissible?: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Complete modal configuration
|
|
128
|
+
*/
|
|
129
|
+
export interface ModalConfig extends ModalContent, ModalBehavior {
|
|
130
|
+
/**
|
|
131
|
+
* Modal actions (buttons)
|
|
132
|
+
*/
|
|
133
|
+
actions?: ModalAction[];
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Maximum width (for responsive design)
|
|
137
|
+
*/
|
|
138
|
+
maxWidth?: number;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Test ID for E2E testing
|
|
142
|
+
*/
|
|
143
|
+
testID?: string;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Custom container style
|
|
147
|
+
*/
|
|
148
|
+
containerStyle?: object;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Custom content style
|
|
152
|
+
*/
|
|
153
|
+
contentStyle?: object;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Modal state for useModalState hook
|
|
158
|
+
*/
|
|
159
|
+
export interface ModalState {
|
|
160
|
+
/**
|
|
161
|
+
* Modal visibility
|
|
162
|
+
*/
|
|
163
|
+
visible: boolean;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Current modal configuration
|
|
167
|
+
*/
|
|
168
|
+
config: ModalConfig | null;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Show modal with configuration
|
|
172
|
+
*/
|
|
173
|
+
show: (config: ModalConfig) => void;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Hide modal
|
|
177
|
+
*/
|
|
178
|
+
hide: () => void;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Update modal configuration
|
|
182
|
+
*/
|
|
183
|
+
update: (config: Partial<ModalConfig>) => void;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Modal result type for async operations
|
|
188
|
+
*/
|
|
189
|
+
export type ModalResult<T = void> =
|
|
190
|
+
| { confirmed: true; data: T }
|
|
191
|
+
| { confirmed: false };
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create a confirmed modal result
|
|
195
|
+
*/
|
|
196
|
+
export function confirmed<T>(data?: T): ModalResult<T> {
|
|
197
|
+
return { confirmed: true, data: data as T };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Create a dismissed modal result
|
|
202
|
+
*/
|
|
203
|
+
export function dismissed(): ModalResult<never> {
|
|
204
|
+
return { confirmed: false };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Common preset configurations
|
|
209
|
+
*/
|
|
210
|
+
export const ModalPresets: {
|
|
211
|
+
/**
|
|
212
|
+
* Alert modal (single OK button)
|
|
213
|
+
*/
|
|
214
|
+
alert: (title: string, message: string, okText?: string) => ModalConfig;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Confirm modal (OK and Cancel buttons)
|
|
218
|
+
*/
|
|
219
|
+
confirm: (
|
|
220
|
+
title: string,
|
|
221
|
+
message: string,
|
|
222
|
+
confirmText?: string,
|
|
223
|
+
cancelText?: string
|
|
224
|
+
) => ModalConfig;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Info modal (just message with icon)
|
|
228
|
+
*/
|
|
229
|
+
info: (title: string, message: string, icon?: string) => ModalConfig;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Error modal (error styling)
|
|
233
|
+
*/
|
|
234
|
+
error: (title: string, message: string) => ModalConfig;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Loading modal (spinner)
|
|
238
|
+
*/
|
|
239
|
+
loading: (message: string) => ModalConfig;
|
|
240
|
+
} = {
|
|
241
|
+
alert: (title, message, okText = 'OK') => ({
|
|
242
|
+
title,
|
|
243
|
+
message,
|
|
244
|
+
actions: [{ label: okText, onPress: () => {}, variant: 'primary' }],
|
|
245
|
+
dismissible: true,
|
|
246
|
+
}),
|
|
247
|
+
|
|
248
|
+
confirm: (title, message, confirmText = 'Confirm', cancelText = 'Cancel') => ({
|
|
249
|
+
title,
|
|
250
|
+
message,
|
|
251
|
+
actions: [
|
|
252
|
+
{ label: cancelText, onPress: () => {}, variant: 'outline' },
|
|
253
|
+
{ label: confirmText, onPress: () => {}, variant: 'primary' },
|
|
254
|
+
],
|
|
255
|
+
dismissible: true,
|
|
256
|
+
}),
|
|
257
|
+
|
|
258
|
+
info: (title, message, icon = 'info') => ({
|
|
259
|
+
title,
|
|
260
|
+
message,
|
|
261
|
+
icon,
|
|
262
|
+
actions: [{ label: 'OK', onPress: () => {}, variant: 'primary' }],
|
|
263
|
+
dismissible: true,
|
|
264
|
+
}),
|
|
265
|
+
|
|
266
|
+
error: (title, message) => ({
|
|
267
|
+
title,
|
|
268
|
+
message,
|
|
269
|
+
icon: 'error',
|
|
270
|
+
iconColor: 'error',
|
|
271
|
+
actions: [{ label: 'OK', onPress: () => {}, variant: 'primary' }],
|
|
272
|
+
dismissible: true,
|
|
273
|
+
}),
|
|
274
|
+
|
|
275
|
+
loading: (message) => ({
|
|
276
|
+
message,
|
|
277
|
+
actions: [],
|
|
278
|
+
dismissible: false,
|
|
279
|
+
closeOnBackdropPress: false,
|
|
280
|
+
closeOnBackPress: false,
|
|
281
|
+
}),
|
|
282
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useModalState Hook
|
|
3
|
+
*
|
|
4
|
+
* Generic modal state management hook.
|
|
5
|
+
* Replaces modal-specific state logic across domains.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const modal = useModalState();
|
|
10
|
+
*
|
|
11
|
+
* // Show modal
|
|
12
|
+
* modal.show({
|
|
13
|
+
* title: 'Confirm',
|
|
14
|
+
* message: 'Are you sure?',
|
|
15
|
+
* actions: [...]
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // Hide modal
|
|
19
|
+
* modal.hide();
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { useCallback, useState } from 'react';
|
|
24
|
+
import type { ModalConfig, ModalState } from './ModalConfig';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generic modal state management hook
|
|
28
|
+
*/
|
|
29
|
+
export function useModalState(initialConfig: ModalConfig | null = null): ModalState {
|
|
30
|
+
const [config, setConfig] = useState<ModalConfig | null>(initialConfig);
|
|
31
|
+
const [visible, setVisible] = useState(false);
|
|
32
|
+
|
|
33
|
+
const show = useCallback((newConfig: ModalConfig) => {
|
|
34
|
+
setConfig(newConfig);
|
|
35
|
+
setVisible(true);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const hide = useCallback(() => {
|
|
39
|
+
setVisible(false);
|
|
40
|
+
// Note: We keep config for animation purposes
|
|
41
|
+
// It will be reset when modal closes completely
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const update = useCallback((updates: Partial<ModalConfig>) => {
|
|
45
|
+
setConfig((prev) => {
|
|
46
|
+
if (!prev) return null;
|
|
47
|
+
return { ...prev, ...updates };
|
|
48
|
+
});
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
visible,
|
|
53
|
+
config,
|
|
54
|
+
show,
|
|
55
|
+
hide,
|
|
56
|
+
update,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extended modal state with result handling
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const modal = useModalStateWithResult<string>();
|
|
66
|
+
*
|
|
67
|
+
* const result = await modal.showAsync({
|
|
68
|
+
* title: 'Input',
|
|
69
|
+
* actions: [
|
|
70
|
+
* { label: 'OK', onPress: (resolve) => resolve('data') }
|
|
71
|
+
* ]
|
|
72
|
+
* });
|
|
73
|
+
*
|
|
74
|
+
* if (result.confirmed) {
|
|
75
|
+
* console.log(result.data);
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function useModalStateWithResult<T = void>() {
|
|
80
|
+
const [config, setConfig] = useState<ModalConfig | null>(null);
|
|
81
|
+
const [visible, setVisible] = useState(false);
|
|
82
|
+
const [resolver, setResolver] = useState<{
|
|
83
|
+
resolve: (result: import('./ModalConfig').ModalResult<T>) => void;
|
|
84
|
+
reject: (error: Error) => void;
|
|
85
|
+
} | null>(null);
|
|
86
|
+
|
|
87
|
+
const showAsync = useCallback(
|
|
88
|
+
(newConfig: ModalConfig): Promise<import('./ModalConfig').ModalResult<T>> => {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
setResolver({ resolve, reject });
|
|
91
|
+
setConfig(newConfig);
|
|
92
|
+
setVisible(true);
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
[]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const hide = useCallback(() => {
|
|
99
|
+
setVisible(false);
|
|
100
|
+
resolver?.resolve({ confirmed: false });
|
|
101
|
+
setResolver(null);
|
|
102
|
+
}, [resolver]);
|
|
103
|
+
|
|
104
|
+
const confirm = useCallback(
|
|
105
|
+
(data?: T) => {
|
|
106
|
+
setVisible(false);
|
|
107
|
+
resolver?.resolve({ confirmed: true, data: data as T });
|
|
108
|
+
setResolver(null);
|
|
109
|
+
},
|
|
110
|
+
[resolver]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const update = useCallback((updates: Partial<ModalConfig>) => {
|
|
114
|
+
setConfig((prev) => {
|
|
115
|
+
if (!prev) return null;
|
|
116
|
+
return { ...prev, ...updates };
|
|
117
|
+
});
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
visible,
|
|
122
|
+
config,
|
|
123
|
+
showAsync,
|
|
124
|
+
hide,
|
|
125
|
+
confirm,
|
|
126
|
+
update,
|
|
127
|
+
};
|
|
128
|
+
}
|