@umituz/react-native-settings 4.21.11 → 4.21.13
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 +11 -2
- package/src/domains/gamification/components/GamificationScreenWrapper.tsx +58 -85
- package/src/domains/gamification/components/index.ts +1 -0
- package/src/domains/gamification/index.ts +4 -11
- package/src/domains/gamification/types/index.ts +18 -0
- package/src/domains/notifications/index.ts +139 -0
- package/src/domains/notifications/infrastructure/config/notificationsConfig.ts +98 -0
- package/src/domains/notifications/infrastructure/hooks/useNotificationSettings.ts +37 -0
- package/src/domains/notifications/infrastructure/services/NotificationBadgeManager.ts +28 -0
- package/src/domains/notifications/infrastructure/services/NotificationManager.ts +138 -0
- package/src/domains/notifications/infrastructure/services/NotificationPermissions.ts +80 -0
- package/src/domains/notifications/infrastructure/services/NotificationScheduler.ts +77 -0
- package/src/domains/notifications/infrastructure/services/NotificationService.ts +50 -0
- package/src/domains/notifications/infrastructure/services/types.ts +176 -0
- package/src/domains/notifications/infrastructure/storage/NotificationsStore.ts +45 -0
- package/src/domains/notifications/infrastructure/utils/dev.ts +25 -0
- package/src/domains/notifications/infrastructure/utils/idGenerator.ts +14 -0
- package/src/domains/notifications/infrastructure/utils/triggerBuilder.ts +45 -0
- package/src/domains/notifications/presentation/components/NotificationsSection.tsx +84 -0
- package/src/domains/notifications/presentation/components/RemindersNavRow.styles.ts +38 -0
- package/src/domains/notifications/presentation/components/RemindersNavRow.tsx +51 -0
- package/src/domains/notifications/presentation/components/SettingRow.tsx +86 -0
- package/src/domains/notifications/presentation/hooks/useNotificationSettingsUI.ts +52 -0
- package/src/domains/notifications/presentation/hooks/useTimePicker.ts +71 -0
- package/src/domains/notifications/presentation/screens/NotificationSettingsScreen.styles.ts +30 -0
- package/src/domains/notifications/presentation/screens/NotificationSettingsScreen.tsx +131 -0
- package/src/domains/notifications/presentation/screens/NotificationsScreen.tsx +107 -0
- package/src/domains/notifications/quietHours/infrastructure/hooks/useQuietHoursActions.ts +52 -0
- package/src/domains/notifications/quietHours/presentation/components/QuietHoursCard.tsx +112 -0
- package/src/domains/notifications/reminders/infrastructure/config/reminderPresets.ts +120 -0
- package/src/domains/notifications/reminders/infrastructure/hooks/useReminderActions.ts +106 -0
- package/src/domains/notifications/reminders/infrastructure/storage/RemindersStore.ts +148 -0
- package/src/domains/notifications/reminders/presentation/components/FormButton.tsx +66 -0
- package/src/domains/notifications/reminders/presentation/components/FrequencySelector.tsx +72 -0
- package/src/domains/notifications/reminders/presentation/components/ReminderForm.tsx +169 -0
- package/src/domains/notifications/reminders/presentation/components/ReminderItem.tsx +130 -0
- package/src/domains/notifications/reminders/presentation/components/TimePresetSelector.tsx +100 -0
- package/src/domains/notifications/reminders/presentation/components/WeekdaySelector.tsx +61 -0
- package/src/domains/notifications/reminders/presentation/screens/ReminderListScreen.tsx +131 -0
- package/src/index.ts +3 -0
- package/src/presentation/navigation/SettingsStackNavigator.tsx +21 -11
- package/src/presentation/screens/components/sections/FeatureSettingsSection.tsx +1 -1
- package/src/domains/gamification/README.md +0 -343
- package/src/domains/gamification/components/GamificationSettingsItem.tsx +0 -33
- package/src/domains/gamification/examples/gamification.config.example.ts +0 -70
- package/src/domains/gamification/examples/localization.example.json +0 -71
- package/src/domains/gamification/types/settings.ts +0 -28
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useQuietHoursActions Hook
|
|
3
|
+
* Manages quiet hours configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from 'react';
|
|
7
|
+
import { usePreferencesStore, useQuietHours } from '../../../reminders/infrastructure/storage/RemindersStore';
|
|
8
|
+
import type { QuietHoursConfig } from '../../../infrastructure/services/types';
|
|
9
|
+
|
|
10
|
+
export const useQuietHoursActions = () => {
|
|
11
|
+
const quietHours = useQuietHours();
|
|
12
|
+
const { updateQuietHours } = usePreferencesStore();
|
|
13
|
+
|
|
14
|
+
const setQuietHoursEnabled = useCallback(async (enabled: boolean): Promise<void> => {
|
|
15
|
+
await updateQuietHours({ ...quietHours, enabled });
|
|
16
|
+
}, [quietHours, updateQuietHours]);
|
|
17
|
+
|
|
18
|
+
const setStartTime = useCallback(async (hour: number, minute: number): Promise<void> => {
|
|
19
|
+
await updateQuietHours({ ...quietHours, startHour: hour, startMinute: minute });
|
|
20
|
+
}, [quietHours, updateQuietHours]);
|
|
21
|
+
|
|
22
|
+
const setEndTime = useCallback(async (hour: number, minute: number): Promise<void> => {
|
|
23
|
+
await updateQuietHours({ ...quietHours, endHour: hour, endMinute: minute });
|
|
24
|
+
}, [quietHours, updateQuietHours]);
|
|
25
|
+
|
|
26
|
+
const setQuietHours = useCallback(async (config: QuietHoursConfig): Promise<void> => {
|
|
27
|
+
await updateQuietHours(config);
|
|
28
|
+
}, [updateQuietHours]);
|
|
29
|
+
|
|
30
|
+
const isInQuietHours = useCallback((): boolean => {
|
|
31
|
+
if (!quietHours.enabled) return false;
|
|
32
|
+
|
|
33
|
+
const now = new Date();
|
|
34
|
+
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
|
35
|
+
const startMinutes = quietHours.startHour * 60 + quietHours.startMinute;
|
|
36
|
+
const endMinutes = quietHours.endHour * 60 + quietHours.endMinute;
|
|
37
|
+
|
|
38
|
+
if (startMinutes <= endMinutes) {
|
|
39
|
+
return currentMinutes >= startMinutes && currentMinutes < endMinutes;
|
|
40
|
+
}
|
|
41
|
+
return currentMinutes >= startMinutes || currentMinutes < endMinutes;
|
|
42
|
+
}, [quietHours]);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
quietHours,
|
|
46
|
+
setQuietHoursEnabled,
|
|
47
|
+
setStartTime,
|
|
48
|
+
setEndTime,
|
|
49
|
+
setQuietHours,
|
|
50
|
+
isInQuietHours,
|
|
51
|
+
};
|
|
52
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuietHoursCard Component
|
|
3
|
+
* Displays and manages quiet hours settings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from 'react-native';
|
|
8
|
+
import { AtomicText, AtomicIcon, AtomicCard } from '@umituz/react-native-design-system';
|
|
9
|
+
import { Switch } from 'react-native';
|
|
10
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system';
|
|
11
|
+
import { SettingRow } from '../../../presentation/components/SettingRow';
|
|
12
|
+
import type { QuietHoursConfig, QuietHoursTranslations } from '../../../infrastructure/services/types';
|
|
13
|
+
|
|
14
|
+
export interface QuietHoursCardProps {
|
|
15
|
+
config: QuietHoursConfig;
|
|
16
|
+
translations: QuietHoursTranslations;
|
|
17
|
+
onToggle: (enabled: boolean) => void;
|
|
18
|
+
onStartTimePress: () => void;
|
|
19
|
+
onEndTimePress: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const formatTime = (hour: number, minute: number): string => {
|
|
23
|
+
return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const QuietHoursCard: React.FC<QuietHoursCardProps> = ({
|
|
27
|
+
config,
|
|
28
|
+
translations,
|
|
29
|
+
onToggle,
|
|
30
|
+
onStartTimePress,
|
|
31
|
+
onEndTimePress,
|
|
32
|
+
}) => {
|
|
33
|
+
const tokens = useAppDesignTokens();
|
|
34
|
+
const styles = useMemo(() => createStyles(tokens), [tokens]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<AtomicCard style={styles.card}>
|
|
38
|
+
<View style={styles.header}>
|
|
39
|
+
<View style={styles.iconContainer}>
|
|
40
|
+
<AtomicIcon name="moon" size="md" color="primary" />
|
|
41
|
+
</View>
|
|
42
|
+
<View style={styles.headerText}>
|
|
43
|
+
<AtomicText type="bodyLarge">{translations.title}</AtomicText>
|
|
44
|
+
<AtomicText type="bodySmall" style={styles.description}>{translations.description}</AtomicText>
|
|
45
|
+
</View>
|
|
46
|
+
<Switch
|
|
47
|
+
value={config.enabled}
|
|
48
|
+
onValueChange={onToggle}
|
|
49
|
+
trackColor={{ false: tokens.colors.surfaceSecondary, true: tokens.colors.primary }}
|
|
50
|
+
thumbColor={tokens.colors.surface}
|
|
51
|
+
/>
|
|
52
|
+
</View>
|
|
53
|
+
|
|
54
|
+
{config.enabled && (
|
|
55
|
+
<View style={styles.timeContainer}>
|
|
56
|
+
<TouchableOpacity style={styles.timeButton} onPress={onStartTimePress} activeOpacity={0.7}>
|
|
57
|
+
<AtomicText type="bodySmall" style={styles.timeLabel}>{translations.startTimeLabel}</AtomicText>
|
|
58
|
+
<AtomicText type="bodyLarge" style={styles.timeValue}>
|
|
59
|
+
{formatTime(config.startHour, config.startMinute)}
|
|
60
|
+
</AtomicText>
|
|
61
|
+
</TouchableOpacity>
|
|
62
|
+
|
|
63
|
+
<View style={styles.timeSeparator}>
|
|
64
|
+
<AtomicIcon name="arrow-forward" size="sm" color="secondary" />
|
|
65
|
+
</View>
|
|
66
|
+
|
|
67
|
+
<TouchableOpacity style={styles.timeButton} onPress={onEndTimePress} activeOpacity={0.7}>
|
|
68
|
+
<AtomicText type="bodySmall" style={styles.timeLabel}>{translations.endTimeLabel}</AtomicText>
|
|
69
|
+
<AtomicText type="bodyLarge" style={styles.timeValue}>
|
|
70
|
+
{formatTime(config.endHour, config.endMinute)}
|
|
71
|
+
</AtomicText>
|
|
72
|
+
</TouchableOpacity>
|
|
73
|
+
</View>
|
|
74
|
+
)}
|
|
75
|
+
</AtomicCard>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const createStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
|
|
80
|
+
StyleSheet.create({
|
|
81
|
+
card: { padding: 16, backgroundColor: tokens.colors.surface },
|
|
82
|
+
header: { flexDirection: 'row', alignItems: 'center' },
|
|
83
|
+
iconContainer: {
|
|
84
|
+
width: 48,
|
|
85
|
+
height: 48,
|
|
86
|
+
borderRadius: 24,
|
|
87
|
+
backgroundColor: tokens.colors.surfaceSecondary,
|
|
88
|
+
justifyContent: 'center',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
marginRight: 12,
|
|
91
|
+
},
|
|
92
|
+
headerText: { flex: 1, marginRight: 12 },
|
|
93
|
+
description: { color: tokens.colors.textSecondary, marginTop: 2 },
|
|
94
|
+
timeContainer: {
|
|
95
|
+
flexDirection: 'row',
|
|
96
|
+
alignItems: 'center',
|
|
97
|
+
marginTop: 16,
|
|
98
|
+
paddingTop: 16,
|
|
99
|
+
borderTopWidth: 1,
|
|
100
|
+
borderTopColor: tokens.colors.surfaceSecondary,
|
|
101
|
+
},
|
|
102
|
+
timeButton: {
|
|
103
|
+
flex: 1,
|
|
104
|
+
backgroundColor: tokens.colors.surfaceSecondary,
|
|
105
|
+
borderRadius: 8,
|
|
106
|
+
padding: 12,
|
|
107
|
+
alignItems: 'center',
|
|
108
|
+
},
|
|
109
|
+
timeLabel: { color: tokens.colors.textSecondary, marginBottom: 4 },
|
|
110
|
+
timeValue: { color: tokens.colors.textPrimary, fontWeight: '600' },
|
|
111
|
+
timeSeparator: { paddingHorizontal: 12 },
|
|
112
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reminder Presets Configuration
|
|
3
|
+
* Default time presets and frequency options for reminders
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TimePreset, ReminderFrequency } from '../../../infrastructure/services/types';
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// DEFAULT TIME PRESETS
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_TIME_PRESETS: TimePreset[] = [
|
|
13
|
+
{
|
|
14
|
+
id: 'morning',
|
|
15
|
+
hour: 8,
|
|
16
|
+
minute: 0,
|
|
17
|
+
labelKey: 'notifications.presets.morning',
|
|
18
|
+
iconName: 'sunny',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'noon',
|
|
22
|
+
hour: 12,
|
|
23
|
+
minute: 0,
|
|
24
|
+
labelKey: 'notifications.presets.noon',
|
|
25
|
+
iconName: 'sunny',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'afternoon',
|
|
29
|
+
hour: 15,
|
|
30
|
+
minute: 0,
|
|
31
|
+
labelKey: 'notifications.presets.afternoon',
|
|
32
|
+
iconName: 'partly-sunny',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'evening',
|
|
36
|
+
hour: 18,
|
|
37
|
+
minute: 0,
|
|
38
|
+
labelKey: 'notifications.presets.evening',
|
|
39
|
+
iconName: 'partly-sunny',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'night',
|
|
43
|
+
hour: 21,
|
|
44
|
+
minute: 0,
|
|
45
|
+
labelKey: 'notifications.presets.night',
|
|
46
|
+
iconName: 'moon',
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// FREQUENCY OPTIONS
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
export interface FrequencyOption {
|
|
55
|
+
id: ReminderFrequency;
|
|
56
|
+
labelKey: string;
|
|
57
|
+
iconName: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const FREQUENCY_OPTIONS: FrequencyOption[] = [
|
|
61
|
+
{
|
|
62
|
+
id: 'once',
|
|
63
|
+
labelKey: 'notifications.frequency.once',
|
|
64
|
+
iconName: 'calendar',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'daily',
|
|
68
|
+
labelKey: 'notifications.frequency.daily',
|
|
69
|
+
iconName: 'repeat',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'weekly',
|
|
73
|
+
labelKey: 'notifications.frequency.weekly',
|
|
74
|
+
iconName: 'calendar',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'monthly',
|
|
78
|
+
labelKey: 'notifications.frequency.monthly',
|
|
79
|
+
iconName: 'calendar',
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// WEEKDAY OPTIONS
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
export interface WeekdayOption {
|
|
88
|
+
id: number;
|
|
89
|
+
labelKey: string;
|
|
90
|
+
shortLabelKey: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const WEEKDAY_OPTIONS: WeekdayOption[] = [
|
|
94
|
+
{ id: 1, labelKey: 'notifications.weekdays.sunday', shortLabelKey: 'notifications.weekdays.sun' },
|
|
95
|
+
{ id: 2, labelKey: 'notifications.weekdays.monday', shortLabelKey: 'notifications.weekdays.mon' },
|
|
96
|
+
{ id: 3, labelKey: 'notifications.weekdays.tuesday', shortLabelKey: 'notifications.weekdays.tue' },
|
|
97
|
+
{ id: 4, labelKey: 'notifications.weekdays.wednesday', shortLabelKey: 'notifications.weekdays.wed' },
|
|
98
|
+
{ id: 5, labelKey: 'notifications.weekdays.thursday', shortLabelKey: 'notifications.weekdays.thu' },
|
|
99
|
+
{ id: 6, labelKey: 'notifications.weekdays.friday', shortLabelKey: 'notifications.weekdays.fri' },
|
|
100
|
+
{ id: 7, labelKey: 'notifications.weekdays.saturday', shortLabelKey: 'notifications.weekdays.sat' },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// HELPER FUNCTIONS
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
export const getTimePresetById = (id: string): TimePreset | undefined => {
|
|
108
|
+
return DEFAULT_TIME_PRESETS.find(preset => preset.id === id);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const formatTime = (hour: number, minute: number): string => {
|
|
112
|
+
const h = hour.toString().padStart(2, '0');
|
|
113
|
+
const m = minute.toString().padStart(2, '0');
|
|
114
|
+
return `${h}:${m}`;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const parseTime = (timeString: string): { hour: number; minute: number } => {
|
|
118
|
+
const [hour, minute] = timeString.split(':').map(Number);
|
|
119
|
+
return { hour: hour || 0, minute: minute || 0 };
|
|
120
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useReminderActions Hook
|
|
3
|
+
* Handles reminder CRUD operations with notification scheduling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from 'react';
|
|
7
|
+
import { useRemindersStore } from '../storage/RemindersStore';
|
|
8
|
+
import { NotificationScheduler } from '../../../infrastructure/services/NotificationScheduler';
|
|
9
|
+
import { generateReminderId } from '../../../infrastructure/utils/idGenerator';
|
|
10
|
+
import { buildTrigger } from '../../../infrastructure/utils/triggerBuilder';
|
|
11
|
+
import type { Reminder, CreateReminderInput, UpdateReminderInput } from '../../../infrastructure/services/types';
|
|
12
|
+
|
|
13
|
+
const scheduler = new NotificationScheduler();
|
|
14
|
+
|
|
15
|
+
export const useReminderActions = () => {
|
|
16
|
+
const { addReminder, updateReminder, deleteReminder, toggleReminder } = useRemindersStore();
|
|
17
|
+
|
|
18
|
+
const createReminder = useCallback(async (input: CreateReminderInput): Promise<Reminder> => {
|
|
19
|
+
const now = new Date().toISOString();
|
|
20
|
+
const reminder: Reminder = {
|
|
21
|
+
id: generateReminderId(),
|
|
22
|
+
...input,
|
|
23
|
+
enabled: true,
|
|
24
|
+
createdAt: now,
|
|
25
|
+
updatedAt: now,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const trigger = buildTrigger(reminder);
|
|
29
|
+
const notificationId = await scheduler.scheduleNotification({
|
|
30
|
+
title: reminder.title,
|
|
31
|
+
body: reminder.body,
|
|
32
|
+
trigger,
|
|
33
|
+
data: { reminderId: reminder.id },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
reminder.notificationId = notificationId;
|
|
37
|
+
await addReminder(reminder);
|
|
38
|
+
|
|
39
|
+
return reminder;
|
|
40
|
+
}, [addReminder]);
|
|
41
|
+
|
|
42
|
+
const editReminder = useCallback(async (id: string, input: UpdateReminderInput): Promise<void> => {
|
|
43
|
+
const { reminders } = useRemindersStore.getState();
|
|
44
|
+
const existing = reminders.find(r => r.id === id);
|
|
45
|
+
if (!existing) return;
|
|
46
|
+
|
|
47
|
+
if (existing.notificationId) {
|
|
48
|
+
await scheduler.cancelNotification(existing.notificationId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const updated: Reminder = { ...existing, ...input, updatedAt: new Date().toISOString() };
|
|
52
|
+
|
|
53
|
+
if (updated.enabled) {
|
|
54
|
+
const trigger = buildTrigger(updated);
|
|
55
|
+
const notificationId = await scheduler.scheduleNotification({
|
|
56
|
+
title: updated.title,
|
|
57
|
+
body: updated.body,
|
|
58
|
+
trigger,
|
|
59
|
+
data: { reminderId: updated.id },
|
|
60
|
+
});
|
|
61
|
+
updated.notificationId = notificationId;
|
|
62
|
+
} else {
|
|
63
|
+
updated.notificationId = undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await updateReminder(id, updated);
|
|
67
|
+
}, [updateReminder]);
|
|
68
|
+
|
|
69
|
+
const removeReminder = useCallback(async (id: string): Promise<void> => {
|
|
70
|
+
const { reminders } = useRemindersStore.getState();
|
|
71
|
+
const reminder = reminders.find(r => r.id === id);
|
|
72
|
+
|
|
73
|
+
if (reminder?.notificationId) {
|
|
74
|
+
await scheduler.cancelNotification(reminder.notificationId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await deleteReminder(id);
|
|
78
|
+
}, [deleteReminder]);
|
|
79
|
+
|
|
80
|
+
const toggleReminderEnabled = useCallback(async (id: string): Promise<void> => {
|
|
81
|
+
const { reminders } = useRemindersStore.getState();
|
|
82
|
+
const reminder = reminders.find(r => r.id === id);
|
|
83
|
+
if (!reminder) return;
|
|
84
|
+
|
|
85
|
+
if (reminder.enabled && reminder.notificationId) {
|
|
86
|
+
await scheduler.cancelNotification(reminder.notificationId);
|
|
87
|
+
await updateReminder(id, { enabled: false, notificationId: undefined });
|
|
88
|
+
} else if (!reminder.enabled) {
|
|
89
|
+
const trigger = buildTrigger(reminder);
|
|
90
|
+
const notificationId = await scheduler.scheduleNotification({
|
|
91
|
+
title: reminder.title,
|
|
92
|
+
body: reminder.body,
|
|
93
|
+
trigger,
|
|
94
|
+
data: { reminderId: reminder.id },
|
|
95
|
+
});
|
|
96
|
+
await updateReminder(id, { enabled: true, notificationId });
|
|
97
|
+
}
|
|
98
|
+
}, [updateReminder]);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
createReminder,
|
|
102
|
+
editReminder,
|
|
103
|
+
removeReminder,
|
|
104
|
+
toggleReminderEnabled,
|
|
105
|
+
};
|
|
106
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reminders Store - Zustand State Management
|
|
3
|
+
* Manages reminder state with AsyncStorage persistence
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createStore } from '@umituz/react-native-storage';
|
|
7
|
+
import type { Reminder, QuietHoursConfig, NotificationPreferences } from '../../../infrastructure/services/types';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// REMINDERS STORE
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
interface RemindersState {
|
|
14
|
+
reminders: Reminder[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RemindersActions {
|
|
18
|
+
addReminder: (reminder: Reminder) => void;
|
|
19
|
+
updateReminder: (id: string, updates: Partial<Reminder>) => void;
|
|
20
|
+
deleteReminder: (id: string) => void;
|
|
21
|
+
toggleReminder: (id: string) => void;
|
|
22
|
+
resetReminders: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DEFAULT_REMINDERS_STATE: RemindersState = {
|
|
26
|
+
reminders: [],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const useRemindersStore = createStore<RemindersState, RemindersActions>({
|
|
30
|
+
name: 'reminders-store',
|
|
31
|
+
initialState: DEFAULT_REMINDERS_STATE,
|
|
32
|
+
persist: true,
|
|
33
|
+
actions: (set, get) => ({
|
|
34
|
+
addReminder: (reminder: Reminder) => {
|
|
35
|
+
const { reminders } = get();
|
|
36
|
+
set({ reminders: [...reminders, reminder] });
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
updateReminder: (id: string, updates: Partial<Reminder>) => {
|
|
40
|
+
const { reminders } = get();
|
|
41
|
+
set({
|
|
42
|
+
reminders: reminders.map(r =>
|
|
43
|
+
r.id === id ? { ...r, ...updates, updatedAt: new Date().toISOString() } : r
|
|
44
|
+
),
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
deleteReminder: (id: string) => {
|
|
49
|
+
const { reminders } = get();
|
|
50
|
+
set({ reminders: reminders.filter(r => r.id !== id) });
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
toggleReminder: (id: string) => {
|
|
54
|
+
const { reminders } = get();
|
|
55
|
+
set({
|
|
56
|
+
reminders: reminders.map(r =>
|
|
57
|
+
r.id === id ? { ...r, enabled: !r.enabled, updatedAt: new Date().toISOString() } : r
|
|
58
|
+
),
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
resetReminders: () => {
|
|
63
|
+
set({ reminders: [] });
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// PREFERENCES STORE
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
interface PreferencesState {
|
|
73
|
+
preferences: NotificationPreferences;
|
|
74
|
+
isLoading: boolean;
|
|
75
|
+
isInitialized: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface PreferencesActions {
|
|
79
|
+
initialize: () => Promise<void>;
|
|
80
|
+
updatePreferences: (updates: Partial<NotificationPreferences>) => void;
|
|
81
|
+
updateQuietHours: (quietHours: QuietHoursConfig) => void;
|
|
82
|
+
reset: () => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// DEFAULT VALUES
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
const DEFAULT_PREFERENCES: NotificationPreferences = {
|
|
90
|
+
enabled: true,
|
|
91
|
+
sound: true,
|
|
92
|
+
vibration: true,
|
|
93
|
+
quietHours: {
|
|
94
|
+
enabled: false,
|
|
95
|
+
startHour: 22,
|
|
96
|
+
startMinute: 0,
|
|
97
|
+
endHour: 7,
|
|
98
|
+
endMinute: 0,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const initialPreferencesState: PreferencesState = {
|
|
103
|
+
preferences: DEFAULT_PREFERENCES,
|
|
104
|
+
isLoading: true,
|
|
105
|
+
isInitialized: false,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const usePreferencesStore = createStore<PreferencesState, PreferencesActions>({
|
|
109
|
+
name: 'preferences-store',
|
|
110
|
+
initialState: initialPreferencesState,
|
|
111
|
+
persist: true,
|
|
112
|
+
actions: (set, get) => ({
|
|
113
|
+
initialize: async () => {
|
|
114
|
+
set({ isLoading: false, isInitialized: true });
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
updatePreferences: (updates: Partial<NotificationPreferences>) => {
|
|
118
|
+
const { preferences } = get();
|
|
119
|
+
set({ preferences: { ...preferences, ...updates } });
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
updateQuietHours: (quietHours: QuietHoursConfig) => {
|
|
123
|
+
const { preferences } = get();
|
|
124
|
+
set({ preferences: { ...preferences, quietHours } });
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
reset: () => {
|
|
128
|
+
set({ preferences: DEFAULT_PREFERENCES });
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// SELECTOR HOOKS - REMINDERS
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
export const useReminders = () => useRemindersStore(state => state.reminders);
|
|
138
|
+
export const useEnabledReminders = () => useRemindersStore(state => state.reminders.filter(r => r.enabled));
|
|
139
|
+
export const useReminderById = (id: string) => useRemindersStore(state => state.reminders.find(r => r.id === id));
|
|
140
|
+
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// SELECTOR HOOKS - PREFERENCES
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
export const useNotificationPreferences = () => usePreferencesStore(state => state.preferences);
|
|
146
|
+
export const useQuietHours = () => usePreferencesStore(state => state.preferences.quietHours);
|
|
147
|
+
export const useRemindersLoading = () => usePreferencesStore(state => state.isLoading);
|
|
148
|
+
export const useRemindersInitialized = () => usePreferencesStore(state => state.isInitialized);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormButton Component
|
|
3
|
+
* Simple button for forms
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
7
|
+
import { TouchableOpacity, StyleSheet } from 'react-native';
|
|
8
|
+
import { AtomicText } from '@umituz/react-native-design-system';
|
|
9
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system';
|
|
10
|
+
|
|
11
|
+
export interface FormButtonProps {
|
|
12
|
+
label: string;
|
|
13
|
+
onPress: () => void;
|
|
14
|
+
variant?: 'primary' | 'secondary';
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const FormButton: React.FC<FormButtonProps> = ({
|
|
19
|
+
label,
|
|
20
|
+
onPress,
|
|
21
|
+
variant = 'primary',
|
|
22
|
+
disabled = false,
|
|
23
|
+
}) => {
|
|
24
|
+
const tokens = useAppDesignTokens();
|
|
25
|
+
const styles = useMemo(() => createStyles(tokens), [tokens]);
|
|
26
|
+
|
|
27
|
+
const isPrimary = variant === 'primary';
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<TouchableOpacity
|
|
31
|
+
style={[
|
|
32
|
+
styles.button,
|
|
33
|
+
isPrimary ? styles.primaryButton : styles.secondaryButton,
|
|
34
|
+
disabled ? styles.disabled : undefined,
|
|
35
|
+
]}
|
|
36
|
+
onPress={onPress}
|
|
37
|
+
disabled={disabled}
|
|
38
|
+
activeOpacity={0.7}
|
|
39
|
+
>
|
|
40
|
+
<AtomicText
|
|
41
|
+
type="bodyMedium"
|
|
42
|
+
style={[styles.label, isPrimary ? styles.primaryLabel : styles.secondaryLabel]}
|
|
43
|
+
>
|
|
44
|
+
{label}
|
|
45
|
+
</AtomicText>
|
|
46
|
+
</TouchableOpacity>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const createStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
|
|
51
|
+
StyleSheet.create({
|
|
52
|
+
button: {
|
|
53
|
+
flex: 1,
|
|
54
|
+
paddingVertical: 14,
|
|
55
|
+
paddingHorizontal: 16,
|
|
56
|
+
borderRadius: 10,
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
justifyContent: 'center',
|
|
59
|
+
},
|
|
60
|
+
primaryButton: { backgroundColor: tokens.colors.primary },
|
|
61
|
+
secondaryButton: { backgroundColor: tokens.colors.surfaceSecondary },
|
|
62
|
+
disabled: { opacity: 0.5 },
|
|
63
|
+
label: { fontWeight: '600' },
|
|
64
|
+
primaryLabel: { color: tokens.colors.surface },
|
|
65
|
+
secondaryLabel: { color: tokens.colors.textPrimary },
|
|
66
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FrequencySelector Component
|
|
3
|
+
* Allows selection of reminder frequency
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from 'react-native';
|
|
8
|
+
import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system';
|
|
9
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system';
|
|
10
|
+
import type { ReminderFrequency } from '../../../infrastructure/services/types';
|
|
11
|
+
import type { FrequencyOption } from '../../infrastructure/config/reminderPresets';
|
|
12
|
+
|
|
13
|
+
export interface FrequencySelectorProps {
|
|
14
|
+
options: FrequencyOption[];
|
|
15
|
+
selectedFrequency: ReminderFrequency;
|
|
16
|
+
onSelect: (frequency: ReminderFrequency) => void;
|
|
17
|
+
getLabel: (labelKey: string) => string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const FrequencySelector: React.FC<FrequencySelectorProps> = ({
|
|
21
|
+
options,
|
|
22
|
+
selectedFrequency,
|
|
23
|
+
onSelect,
|
|
24
|
+
getLabel,
|
|
25
|
+
}) => {
|
|
26
|
+
const tokens = useAppDesignTokens();
|
|
27
|
+
const styles = useMemo(() => createStyles(tokens), [tokens]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<View style={styles.container}>
|
|
31
|
+
{options.map(option => {
|
|
32
|
+
const isSelected = selectedFrequency === option.id;
|
|
33
|
+
return (
|
|
34
|
+
<TouchableOpacity
|
|
35
|
+
key={option.id}
|
|
36
|
+
style={[styles.button, isSelected ? styles.selectedButton : undefined]}
|
|
37
|
+
onPress={() => onSelect(option.id)}
|
|
38
|
+
activeOpacity={0.7}
|
|
39
|
+
>
|
|
40
|
+
<AtomicIcon
|
|
41
|
+
name={option.iconName}
|
|
42
|
+
size="sm"
|
|
43
|
+
color={isSelected ? 'onSurface' : 'secondary'}
|
|
44
|
+
/>
|
|
45
|
+
<AtomicText type="bodySmall" style={isSelected ? styles.selectedText : styles.text}>
|
|
46
|
+
{getLabel(option.labelKey)}
|
|
47
|
+
</AtomicText>
|
|
48
|
+
</TouchableOpacity>
|
|
49
|
+
);
|
|
50
|
+
})}
|
|
51
|
+
</View>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const createStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
|
|
56
|
+
StyleSheet.create({
|
|
57
|
+
container: { flexDirection: 'row', gap: 8 },
|
|
58
|
+
button: {
|
|
59
|
+
flex: 1,
|
|
60
|
+
flexDirection: 'row',
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
justifyContent: 'center',
|
|
63
|
+
paddingVertical: 10,
|
|
64
|
+
paddingHorizontal: 8,
|
|
65
|
+
borderRadius: 8,
|
|
66
|
+
backgroundColor: tokens.colors.surfaceSecondary,
|
|
67
|
+
gap: 6,
|
|
68
|
+
},
|
|
69
|
+
selectedButton: { backgroundColor: tokens.colors.primary },
|
|
70
|
+
text: { color: tokens.colors.textSecondary },
|
|
71
|
+
selectedText: { color: tokens.colors.surface },
|
|
72
|
+
});
|