@umituz/react-native-settings 5.4.9 → 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 -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 +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
|
@@ -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,113 @@ 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;
|
|
86
87
|
}
|
|
87
|
-
}
|
|
88
88
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (isDev()) {
|
|
97
|
-
console.error('[RatingService] Failed to mark prompt shown:', eventType, error);
|
|
98
|
-
}
|
|
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
|
+
});
|
|
99
96
|
}
|
|
100
|
-
}
|
|
101
97
|
|
|
102
|
-
/**
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (isDev()) {
|
|
110
|
-
console.error('[RatingService] Failed to mark as rated:', error);
|
|
111
|
-
}
|
|
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
|
+
});
|
|
112
105
|
}
|
|
113
|
-
}
|
|
114
106
|
|
|
115
|
-
/**
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
});
|
|
125
130
|
}
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
/**
|
|
129
|
-
*
|
|
134
|
+
* Singleton instance
|
|
130
135
|
*/
|
|
131
|
-
|
|
132
|
-
return getRatingState(eventType);
|
|
133
|
-
}
|
|
136
|
+
let ratingServiceInstance: RatingService | null = null;
|
|
134
137
|
|
|
135
138
|
/**
|
|
136
|
-
*
|
|
139
|
+
* Get rating service singleton instance
|
|
137
140
|
*/
|
|
138
|
-
export
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
} catch (error) {
|
|
142
|
-
if (isDev()) {
|
|
143
|
-
console.error('[RatingService] Failed to reset:', eventType, error);
|
|
144
|
-
}
|
|
141
|
+
export function getRatingService(): RatingService {
|
|
142
|
+
if (!ratingServiceInstance) {
|
|
143
|
+
ratingServiceInstance = new RatingService();
|
|
145
144
|
}
|
|
145
|
+
return ratingServiceInstance;
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Default export for backward compatibility
|
|
150
|
+
*/
|
|
151
|
+
export const ratingService = getRatingService();
|
|
@@ -37,8 +37,8 @@ export type { StarRatingProps } from './presentation/components/StarRating';
|
|
|
37
37
|
// PRESENTATION LAYER - Components (App Store Rating)
|
|
38
38
|
// =============================================================================
|
|
39
39
|
|
|
40
|
-
export {
|
|
41
|
-
export type {
|
|
40
|
+
export { useRatingPromptModal } from './presentation/hooks/useRatingPromptModal';
|
|
41
|
+
export type { RatingPromptOptions } from './presentation/hooks/useRatingPromptModal';
|
|
42
42
|
|
|
43
43
|
// =============================================================================
|
|
44
44
|
// PRESENTATION LAYER - Hooks (App Store Rating)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useRatingPromptModal Hook
|
|
3
|
+
*
|
|
4
|
+
* Refactored to use GenericModal and ModalConfig.
|
|
5
|
+
* Replaces old RatingPromptModal component.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const { RatingPrompt, showRatingPrompt } = useRatingPromptModal();
|
|
10
|
+
*
|
|
11
|
+
* return (
|
|
12
|
+
* <>
|
|
13
|
+
* <Button onPress={() => showRatingPrompt({ onPositive: ... })} />
|
|
14
|
+
* <RatingPrompt />
|
|
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
|
+
import type { RatingTranslations } from '../../domain/entities/RatingConfig';
|
|
25
|
+
|
|
26
|
+
export interface RatingPromptOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Callback when user clicks positive (loves the app)
|
|
29
|
+
*/
|
|
30
|
+
onPositive: () => void | Promise<void>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Callback when user clicks negative (doesn't love the app)
|
|
34
|
+
*/
|
|
35
|
+
onNegative: () => void | Promise<void>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Callback when user clicks later
|
|
39
|
+
*/
|
|
40
|
+
onLater: () => void | Promise<void>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* App name for message
|
|
44
|
+
*/
|
|
45
|
+
appName?: string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Custom translations
|
|
49
|
+
*/
|
|
50
|
+
translations?: RatingTranslations;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Custom modal configuration
|
|
54
|
+
*/
|
|
55
|
+
config?: Partial<ModalConfig>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Rating prompt modal hook
|
|
60
|
+
*/
|
|
61
|
+
export function useRatingPromptModal() {
|
|
62
|
+
const modal = useModalState();
|
|
63
|
+
|
|
64
|
+
const showRatingPrompt = useCallback((options: RatingPromptOptions) => {
|
|
65
|
+
const {
|
|
66
|
+
onPositive,
|
|
67
|
+
onNegative,
|
|
68
|
+
onLater,
|
|
69
|
+
appName = 'this app',
|
|
70
|
+
translations,
|
|
71
|
+
config: customConfig,
|
|
72
|
+
} = options;
|
|
73
|
+
|
|
74
|
+
const defaultTranslations: RatingTranslations = {
|
|
75
|
+
title: translations?.title ?? 'Enjoying the app?',
|
|
76
|
+
message:
|
|
77
|
+
translations?.message ??
|
|
78
|
+
`If you love using ${appName}, would you mind taking a moment to rate it?`,
|
|
79
|
+
positiveButton: translations?.positiveButton ?? 'Yes, I love it!',
|
|
80
|
+
negativeButton: translations?.negativeButton ?? 'Not really',
|
|
81
|
+
laterButton: translations?.laterButton ?? 'Maybe later',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const modalConfig: ModalConfig = {
|
|
85
|
+
title: defaultTranslations.title,
|
|
86
|
+
message: defaultTranslations.message,
|
|
87
|
+
icon: 'star',
|
|
88
|
+
iconColor: 'primary',
|
|
89
|
+
actions: [
|
|
90
|
+
{
|
|
91
|
+
label: defaultTranslations.positiveButton,
|
|
92
|
+
onPress: onPositive,
|
|
93
|
+
variant: 'primary',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
label: defaultTranslations.negativeButton,
|
|
97
|
+
onPress: onNegative,
|
|
98
|
+
variant: 'outline',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
label: defaultTranslations.laterButton,
|
|
102
|
+
onPress: onLater,
|
|
103
|
+
variant: 'text',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
dismissible: true,
|
|
107
|
+
...customConfig,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
modal.show(modalConfig);
|
|
111
|
+
}, [modal]);
|
|
112
|
+
|
|
113
|
+
const RatingPrompt = useCallback(() => {
|
|
114
|
+
return <GenericModal state={modal} testID="rating-prompt-modal" />;
|
|
115
|
+
}, [modal]);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
RatingPrompt,
|
|
119
|
+
showRatingPrompt,
|
|
120
|
+
modal,
|
|
121
|
+
};
|
|
122
|
+
}
|
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
|
// =============================================================================
|
|
@@ -1,37 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Settings Service
|
|
3
3
|
*
|
|
4
|
-
* Orchestrates settings operations using SettingsRepository
|
|
4
|
+
* Orchestrates settings operations using SettingsRepository.
|
|
5
|
+
* Refactored to extend BaseService for consistent error handling.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { SettingsRepository } from '../repositories/SettingsRepository';
|
|
9
|
+
import { BaseService } from '../../core/base/BaseService';
|
|
8
10
|
import type { UserSettings, SettingsResult } from '../../application/ports/ISettingsRepository';
|
|
9
11
|
|
|
10
|
-
export class SettingsService {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
12
|
+
export class SettingsService extends BaseService {
|
|
13
|
+
protected serviceName = 'SettingsService';
|
|
14
|
+
private repository: SettingsRepository;
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
this.repository = new SettingsRepository();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async getSettings(userId: string): Promise<SettingsResult<UserSettings>> {
|
|
22
|
+
return this.execute('getSettings', async () => {
|
|
23
|
+
return await this.repository.getSettings(userId);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async saveSettings(settings: UserSettings): Promise<SettingsResult<void>> {
|
|
28
|
+
return this.execute('saveSettings', async () => {
|
|
29
|
+
return await this.repository.saveSettings(settings);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async resetSettings(userId: string): Promise<SettingsResult<void>> {
|
|
34
|
+
return this.execute('resetSettings', async () => {
|
|
35
|
+
return await this.repository.deleteSettings(userId);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
let settingsServiceInstance: SettingsService | null = null;
|
|
31
41
|
|
|
32
42
|
export function getSettingsService(): SettingsService {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
if (!settingsServiceInstance) {
|
|
44
|
+
settingsServiceInstance = new SettingsService();
|
|
45
|
+
}
|
|
46
|
+
return settingsServiceInstance;
|
|
37
47
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenericModal Component
|
|
3
|
+
*
|
|
4
|
+
* Universal modal component that works with ModalConfig.
|
|
5
|
+
* Replaces all custom modal implementations.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const modal = useModalState();
|
|
10
|
+
*
|
|
11
|
+
* return (
|
|
12
|
+
* <>
|
|
13
|
+
* <Button onPress={() => modal.show(ModalPresets.confirm(...))} />
|
|
14
|
+
* <GenericModal state={modal} />
|
|
15
|
+
* </>
|
|
16
|
+
* );
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import React, { useMemo } from 'react';
|
|
21
|
+
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
|
22
|
+
import { BaseModal } from '@umituz/react-native-design-system/molecules';
|
|
23
|
+
import { AtomicText, AtomicButton, AtomicIcon } from '@umituz/react-native-design-system/atoms';
|
|
24
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system/theme';
|
|
25
|
+
import { useResponsive } from '@umituz/react-native-design-system/responsive';
|
|
26
|
+
import type { ModalConfig, ModalState } from '../../core/patterns/Modal/ModalConfig';
|
|
27
|
+
|
|
28
|
+
export interface GenericModalProps {
|
|
29
|
+
/**
|
|
30
|
+
* Modal state from useModalState hook
|
|
31
|
+
*/
|
|
32
|
+
state: ModalState;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Custom container style
|
|
36
|
+
*/
|
|
37
|
+
containerStyle?: object;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Test ID for E2E testing
|
|
41
|
+
*/
|
|
42
|
+
testID?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const GenericModal: React.FC<GenericModalProps> = ({
|
|
46
|
+
state,
|
|
47
|
+
containerStyle,
|
|
48
|
+
testID = 'generic-modal',
|
|
49
|
+
}) => {
|
|
50
|
+
const tokens = useAppDesignTokens();
|
|
51
|
+
const responsive = useResponsive();
|
|
52
|
+
const { visible, config } = state;
|
|
53
|
+
|
|
54
|
+
const styles = useMemo(() => getStyles(tokens), [tokens]);
|
|
55
|
+
|
|
56
|
+
if (!config) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const {
|
|
61
|
+
title,
|
|
62
|
+
subtitle,
|
|
63
|
+
message,
|
|
64
|
+
icon,
|
|
65
|
+
iconColor,
|
|
66
|
+
header,
|
|
67
|
+
body,
|
|
68
|
+
footer,
|
|
69
|
+
actions = [],
|
|
70
|
+
dismissible = true,
|
|
71
|
+
closeOnBackdropPress = true,
|
|
72
|
+
closeOnBackPress = true,
|
|
73
|
+
maxWidth,
|
|
74
|
+
} = config;
|
|
75
|
+
|
|
76
|
+
const handleDismiss = () => {
|
|
77
|
+
if (dismissible) {
|
|
78
|
+
state.hide();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const modalWidth = maxWidth ?? responsive.maxContentWidth * 0.9;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<BaseModal
|
|
86
|
+
visible={visible}
|
|
87
|
+
onClose={handleDismiss}
|
|
88
|
+
closeOnBackdropPress={closeOnBackdropPress}
|
|
89
|
+
closeOnBackPress={closeOnBackPress}
|
|
90
|
+
>
|
|
91
|
+
<View
|
|
92
|
+
style={[
|
|
93
|
+
styles.container,
|
|
94
|
+
{
|
|
95
|
+
backgroundColor: tokens.colors.surface,
|
|
96
|
+
borderRadius: tokens.borders.radius.xl,
|
|
97
|
+
padding: tokens.spacing.lg,
|
|
98
|
+
maxWidth: modalWidth,
|
|
99
|
+
width: '90%',
|
|
100
|
+
},
|
|
101
|
+
containerStyle,
|
|
102
|
+
]}
|
|
103
|
+
testID={testID}
|
|
104
|
+
>
|
|
105
|
+
{/* Custom Header */}
|
|
106
|
+
{header ? (
|
|
107
|
+
header
|
|
108
|
+
) : (
|
|
109
|
+
<>
|
|
110
|
+
{/* Icon */}
|
|
111
|
+
{icon && (
|
|
112
|
+
<View style={styles.iconContainer}>
|
|
113
|
+
<AtomicIcon
|
|
114
|
+
name={icon}
|
|
115
|
+
size="xl"
|
|
116
|
+
color={iconColor || 'primary'}
|
|
117
|
+
/>
|
|
118
|
+
</View>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{/* Title and Subtitle */}
|
|
122
|
+
{title && (
|
|
123
|
+
<AtomicText
|
|
124
|
+
type="headlineMedium"
|
|
125
|
+
color="onSurface"
|
|
126
|
+
style={[styles.title, { marginBottom: subtitle ? tokens.spacing.xs : tokens.spacing.md }]}
|
|
127
|
+
>
|
|
128
|
+
{title}
|
|
129
|
+
</AtomicText>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{subtitle && (
|
|
133
|
+
<AtomicText
|
|
134
|
+
type="bodyMedium"
|
|
135
|
+
color="onSurfaceVariant"
|
|
136
|
+
style={[styles.subtitle, { marginBottom: tokens.spacing.md }]}
|
|
137
|
+
>
|
|
138
|
+
{subtitle}
|
|
139
|
+
</AtomicText>
|
|
140
|
+
)}
|
|
141
|
+
</>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{/* Custom Body or Message */}
|
|
145
|
+
{body ? (
|
|
146
|
+
body
|
|
147
|
+
) : message && (
|
|
148
|
+
<AtomicText
|
|
149
|
+
type="bodyMedium"
|
|
150
|
+
color="onSurfaceVariant"
|
|
151
|
+
style={[styles.message, { marginBottom: actions.length > 0 ? tokens.spacing.lg : 0 }]}
|
|
152
|
+
>
|
|
153
|
+
{message}
|
|
154
|
+
</AtomicText>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{/* Custom Footer or Actions */}
|
|
158
|
+
{footer ? (
|
|
159
|
+
footer
|
|
160
|
+
) : actions.length > 0 && (
|
|
161
|
+
<View style={[styles.buttonContainer, { gap: tokens.spacing.sm }]}>
|
|
162
|
+
{actions.map((action, index) => (
|
|
163
|
+
<AtomicButton
|
|
164
|
+
key={index}
|
|
165
|
+
variant={action.variant || 'primary'}
|
|
166
|
+
onPress={async () => {
|
|
167
|
+
await action.onPress();
|
|
168
|
+
if (dismissible) {
|
|
169
|
+
state.hide();
|
|
170
|
+
}
|
|
171
|
+
}}
|
|
172
|
+
disabled={action.disabled}
|
|
173
|
+
loading={action.loading}
|
|
174
|
+
style={styles.button}
|
|
175
|
+
testID={action.testID}
|
|
176
|
+
>
|
|
177
|
+
{action.label}
|
|
178
|
+
</AtomicButton>
|
|
179
|
+
))}
|
|
180
|
+
</View>
|
|
181
|
+
)}
|
|
182
|
+
</View>
|
|
183
|
+
</BaseModal>
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const getStyles = (_tokens: ReturnType<typeof useAppDesignTokens>) =>
|
|
188
|
+
StyleSheet.create({
|
|
189
|
+
container: {
|
|
190
|
+
alignItems: 'center',
|
|
191
|
+
},
|
|
192
|
+
iconContainer: {
|
|
193
|
+
marginBottom: 16,
|
|
194
|
+
},
|
|
195
|
+
title: {
|
|
196
|
+
textAlign: 'center',
|
|
197
|
+
},
|
|
198
|
+
subtitle: {
|
|
199
|
+
textAlign: 'center',
|
|
200
|
+
},
|
|
201
|
+
message: {
|
|
202
|
+
textAlign: 'center',
|
|
203
|
+
lineHeight: 24,
|
|
204
|
+
},
|
|
205
|
+
buttonContainer: {
|
|
206
|
+
width: '100%',
|
|
207
|
+
marginTop: 16,
|
|
208
|
+
},
|
|
209
|
+
button: {
|
|
210
|
+
width: '100%',
|
|
211
|
+
},
|
|
212
|
+
});
|