api: rename the notification mute to pushEnabled
The preference only ever stopped delivery, so it is now named for what it does: a push-scoped mute, renamed in place so the column keeps its values. The daily reminder is scheduled whenever a time is set, which lets someone who silences push keep the record in the app, and Settings splits the two into independent rows
Pedro Lucas Porcellis porcellis@eletrotupi.com · 11 days ago
30f98b88ba3e0926a1a93ed3c7a9e2618d9ff920 Browse filesParents
dafa6f8Changed files
15
Additions
+127
Deletions
-43
api/prisma/migrations/20261017033204_rename_notifications_enabled_to_push_enabled/migration.sql
added+3 /dev/null → api/prisma/migrations/20261017033204_rename_notifications_enabled_to_push_enabled/migration.sql
1 +
-- The preference gates push only, so it is named for what it does. A rename,
2 +
-- not a drop and add: the column keeps its values.
3 +
ALTER TABLE "users" RENAME COLUMN "notifications_enabled" TO "push_enabled";
api/prisma/schema.prisma
1 1
generator client {
2 2
provider = "prisma-client-js"
3 3
//output = "../src/generated/prisma"
4 4
}
5 5
23 23
activationCode String? @unique @map("activation_code")
24 24
activationCodeExpiresAt DateTime? @map("activation_code_expires_at")
25 25
active Boolean @default(false) @map("active")
26 26
27 27
pushToken String? @map("push_token")
28 -
notificationsEnabled Boolean @default(true) @map("notifications_enabled")
28 +
pushEnabled Boolean @default(true) @map("push_enabled")
29 29
dailyReminderTime String? @map("daily_reminder_time")
30 30
weeklyResumeEnabled Boolean @default(false) @map("weekly_resume_enabled")
31 31
32 32
onboardingSeenAt DateTime? @map("onboarding_seen_at")
33 33
termsAcceptedAt DateTime? @map("terms_accepted_at")
334 334
THREE_TIMES_DAILY
335 335
WEEKLY
336 336
BIWEEKLY
337 337
MONTHLY
338 338
}
api/src/lib/serializers/user.ts
1 1
import { Prisma, User } from '@prisma/client';
2 2
3 3
// DB-level projection of the allow-list below — keep the two in sync.
4 4
export const publicUserSelect = {
5 5
id: true,
8 8
lastName: true,
9 9
updatedAt: true,
10 10
avatarKey: true,
11 11
avatarURL: true,
12 12
active: true,
13 -
notificationsEnabled: true,
13 +
pushEnabled: true,
14 14
dailyReminderTime: true,
15 15
weeklyResumeEnabled: true,
16 16
onboardingSeenAt: true,
17 17
} satisfies Prisma.UserSelect;
18 18
25 25
lastName: user.lastName,
26 26
updatedAt: user.updatedAt,
27 27
avatarKey: user.avatarKey,
28 28
avatarURL: user.avatarURL,
29 29
active: user.active,
30 -
notificationsEnabled: user.notificationsEnabled,
30 +
pushEnabled: user.pushEnabled,
31 31
dailyReminderTime: user.dailyReminderTime,
32 32
weeklyResumeEnabled: user.weeklyResumeEnabled,
33 33
onboardingSeenAt: user.onboardingSeenAt,
34 34
});
api/src/schemas/user.schema.ts
1 1
import { z } from "zod";
2 2
3 3
export const CreateUserSchema = z.object({
4 4
firstName: z.string(),
5 5
lastName: z.string().optional(),
17 17
18 18
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
19 19
export type UpdateUserInput = z.infer<typeof UpdateUserSchema>;
20 20
21 21
export const UpdateUserPreferencesSchema = z.object({
22 -
notificationsEnabled: z.boolean().optional(),
22 +
pushEnabled: z.boolean().optional(),
23 23
dailyReminderTime: z.string().regex(/^\d{2}:\d{2}$/).nullable().optional(),
24 24
weeklyResumeEnabled: z.boolean().optional(),
25 25
});
26 26
27 27
export type UpdateUserPreferencesInput = z.infer<typeof UpdateUserPreferencesSchema>;
api/src/services/dailyReminder.sync.ts
1 1
import { User } from "@prisma/client";
2 2
import { getQueue } from "@app/lib/queue/QueueRegistry";
3 3
import { DailyReminderJobName } from "@app/lib/queue/types";
4 4
import { logger } from "@app/lib/logger";
5 5
19 19
if (job.id?.includes(`daily-reminder-user-${user.id}`)) {
20 20
await queue.removeRepeatableByKey(job.key);
21 21
}
22 22
}
23 23
24 -
if (!user.notificationsEnabled || !user.dailyReminderTime) {
24 +
if (!user.dailyReminderTime) {
25 25
logger.info(
26 -
{
27 -
userId: user.id,
28 -
enabled: user.notificationsEnabled,
29 -
time: user.dailyReminderTime,
30 -
},
26 +
{ userId: user.id, time: user.dailyReminderTime },
31 27
"[daily-sync] user skipped",
32 28
);
33 29
34 30
return;
35 31
}
48 44
logger.info(
49 45
{ userId: user.id, time: user.dailyReminderTime, cron },
50 46
"[daily-sync] scheduled daily reminder",
51 47
);
52 48
}
api/src/services/export/table.descriptors.ts
1 1
import {
2 2
Activity,
3 3
Appointment,
4 4
CareAction,
5 5
Insight,
42 42
{ header: "Nome", value: (user) => user.firstName },
43 43
{ header: "Sobrenome", value: (user) => user.lastName },
44 44
{ header: "Email", value: (user) => user.email },
45 45
{ header: "Ativo", value: (user) => booleanLabel(user.active) },
46 46
{
47 -
header: "Notificações",
48 -
value: (user) => booleanLabel(user.notificationsEnabled),
47 +
header: "Notificações por push",
48 +
value: (user) => booleanLabel(user.pushEnabled),
49 49
},
50 50
{ header: "Lembrete diário", value: (user) => user.dailyReminderTime },
51 51
{
52 52
header: "Resumo semanal",
53 53
value: (user) => booleanLabel(user.weeklyResumeEnabled),
206 206
{ header: "Início do período", value: (insight) => insight.periodStart },
207 207
{ header: "Fim do período", value: (insight) => insight.periodEnd },
208 208
{ header: "Gerado em", value: (insight) => insight.generatedAt },
209 209
],
210 210
};
api/src/services/notification.service.ts
1 1
import { Notification, NotificationKind } from "@prisma/client";
2 2
import { endOfDay } from "date-fns";
3 3
4 4
import { prisma } from "@app/lib/prisma";
5 5
import { APP_TIMEZONE } from "@app/lib/appTime";
44 44
): Promise<Notification | null> => {
45 45
const { userId, kind, title, body, scheduledFor, regimenId = null } = input;
46 46
47 47
const user = await prisma.user.findUnique({
48 48
where: { id: userId },
49 -
select: { pushToken: true, notificationsEnabled: true },
49 +
select: { pushToken: true, pushEnabled: true },
50 50
});
51 51
52 52
if (!user) {
53 53
logger.warn({ userId, kind }, "[notification] user not found, skipping");
54 54
67 67
regimenId,
68 68
expiresAt: rule.expiresAt(scheduledFor),
69 69
},
70 70
});
71 71
72 -
if (!user.notificationsEnabled || !user.pushToken) {
72 +
if (!user.pushEnabled || !user.pushToken) {
73 73
logger.info(
74 74
{ notificationId: notification.id, userId, kind },
75 75
"[notification] stored without push"
76 76
);
77 77
107 107
return prisma.notification.update({
108 108
where: { id: notification.id },
109 109
data: { pushedAt: new Date() },
110 110
});
111 111
};
api/tests/jobs/export-data.test.ts
1 1
import { describe, it, expect, afterAll, beforeEach, vi } from 'vitest'
2 2
import JSZip from 'jszip'
3 3
import { exportProcessor } from '@app/lib/queue/processors/exports'
4 4
import { ExportJobName } from '@app/lib/queue/types'
5 5
import { buildPrisma, cleanupTestDb, createTestUser } from '../test-helper'
83 83
84 84
const lines = csvLines(csv)
85 85
expect(lines).toHaveLength(2)
86 86
expect(lines[0]).toContain('Nome')
87 87
expect(lines[0]).toContain('Email')
88 +
expect(lines[0]).toContain('Notificações por push')
88 89
89 90
const [row] = lines.slice(1)
90 91
expect(row).toContain('"Ana"')
91 92
expect(row).toContain('"Silva"')
92 93
expect(row).toContain('"export@example.com"')
93 -
expect(row).toContain('"Sim"') // notifications default to enabled
94 +
expect(row).toContain('"Sim"') // push defaults to enabled
94 95
expect(row).toContain('"Não"') // freshly created users are inactive
95 96
96 -
// Secrets must never travel in the export
97 +
// Secrets must never travel in the export. The push token is caught by the
98 +
// `token` check below; "push" alone is now a legitimate column word.
97 99
const lower = csv.toLowerCase()
98 100
expect(lower).not.toContain('senha')
99 101
expect(lower).not.toContain('token')
100 -
expect(lower).not.toContain('push')
101 102
expect(lower).not.toContain('avatar')
102 103
103 104
const manifest = await zip.file('leia-me.md')!.async('string')
104 105
expect(manifest).toContain('`profile.csv`')
105 106
expect(manifest).toContain('1 registro')
534 535
await expect(
535 536
exportProcessor({ name: 'export:unknown', data: {} } as any)
536 537
).rejects.toThrow('Unknown export job')
537 538
})
538 539
})
api/tests/jobs/push-notifications.test.ts
1 1
import { describe, it, expect, afterAll, beforeEach, vi } from 'vitest'
2 2
import { medicineReminderProcessor } from '@app/lib/queue/processors/medicineReminders'
3 3
import { dailyReminderProcessor } from '@app/lib/queue/processors/dailyReminders'
4 4
import { buildPrisma, cleanupTestDb, createTestUser } from '../test-helper'
5 5
117 117
118 118
describe('dailyReminderProcessor', () => {
119 119
it('stores a daily notification for the reminder time and pushes it', async () => {
120 120
const user = await createTestUser(db, {
121 121
pushToken: 'ExponentPushToken[daily123]',
122 -
notificationsEnabled: true,
122 +
pushEnabled: true,
123 123
})
124 124
125 125
await dailyReminderProcessor({
126 126
data: { userId: user.id, scheduledTime: '20:00' },
127 127
} as any)
149 149
})
150 150
151 151
it('stores the notification without pushing when notifications are disabled', async () => {
152 152
const user = await createTestUser(db, {
153 153
pushToken: 'ExponentPushToken[disabled]',
154 -
notificationsEnabled: false,
154 +
pushEnabled: false,
155 155
})
156 156
157 157
await dailyReminderProcessor({
158 158
data: { userId: user.id, scheduledTime: '20:00' },
159 159
} as any)
171 171
172 172
expect(mockedSend).not.toHaveBeenCalled()
173 173
expect(await db.notification.count()).toBe(0)
174 174
})
175 175
})
api/tests/notifications/create.test.ts
1 1
import { describe, it, expect, afterAll, beforeEach, vi } from 'vitest'
2 2
import { NotificationKind } from '@prisma/client'
3 3
import { buildPrisma, cleanupTestDb, createTestUser } from '../test-helper'
4 4
5 5
vi.mock('@app/lib/sendPushNotification', () => ({
65 65
})
66 66
67 67
it('records when a push was dispatched', async () => {
68 68
const user = await createTestUser(db, {
69 69
pushToken: 'ExponentPushToken[abc123]',
70 -
notificationsEnabled: true,
70 +
pushEnabled: true,
71 71
})
72 72
73 73
await createNotification(dailyInput(user.id))
74 74
75 75
expect(mockedSend).toHaveBeenCalledOnce()
78 78
})
79 79
80 80
it('stores the notification but does not push when notifications are disabled', async () => {
81 81
const user = await createTestUser(db, {
82 82
pushToken: 'ExponentPushToken[disabled]',
83 -
notificationsEnabled: false,
83 +
pushEnabled: false,
84 84
})
85 85
86 86
await createNotification(dailyInput(user.id))
87 87
88 88
expect(mockedSend).not.toHaveBeenCalled()
90 90
expect(row).not.toBeNull()
91 91
expect(row?.pushedAt).toBeNull()
92 92
})
93 93
94 94
it('stores the notification but does not push without a push token', async () => {
95 -
const user = await createTestUser(db, { notificationsEnabled: true })
95 +
const user = await createTestUser(db, { pushEnabled: true })
96 96
97 97
await createNotification(dailyInput(user.id))
98 98
99 99
expect(mockedSend).not.toHaveBeenCalled()
100 100
const row = await db.notification.findFirst()
103 103
})
104 104
105 105
it('keeps an unpushed record when the push fails', async () => {
106 106
const user = await createTestUser(db, {
107 107
pushToken: 'ExponentPushToken[boom]',
108 -
notificationsEnabled: true,
108 +
pushEnabled: true,
109 109
})
110 110
mockedSend.mockRejectedValueOnce(new Error('Expo push API returned 500'))
111 111
112 112
await expect(createNotification(dailyInput(user.id))).resolves.toBeDefined()
113 113
122 122
expect(result).toBeNull()
123 123
expect(await db.notification.count()).toBe(0)
124 124
expect(mockedSend).not.toHaveBeenCalled()
125 125
})
126 126
})
api/tests/serializers/user.test.ts
1 1
import { describe, it, expect } from 'vitest'
2 2
import { User } from '@prisma/client'
3 3
import { publicUser } from '@app/lib/serializers/user'
4 4
5 5
const row = {
16 16
avatarURL: 'https://cdn.example.com/avatars/pedro.png',
17 17
activationCode: '123456',
18 18
activationCodeExpiresAt: new Date(),
19 19
active: true,
20 20
pushToken: 'device-token',
21 -
notificationsEnabled: true,
21 +
pushEnabled: true,
22 22
dailyReminderTime: '08:30',
23 23
weeklyResumeEnabled: false,
24 24
onboardingSeenAt: null,
25 25
termsAcceptedAt: new Date(),
26 26
termsVersion: '1',
53 53
]) {
54 54
expect(safe).not.toHaveProperty(field)
55 55
}
56 56
})
57 57
})
api/tests/test-helper.ts
1 1
import { PrismaClient } from '@prisma/client'
2 2
import { PrismaPg } from '@prisma/adapter-pg'
3 3
import jwt from 'jsonwebtoken'
4 4
import bcrypt from 'bcryptjs'
5 5
import { expect } from 'vitest'
23 23
email?: string
24 24
firstName?: string
25 25
lastName?: string
26 26
password?: string
27 27
pushToken?: string
28 -
notificationsEnabled?: boolean
28 +
pushEnabled?: boolean
29 29
} = {}
30 30
) {
31 -
const { password, pushToken, notificationsEnabled, ...rest } = overrides
31 +
const { password, pushToken, pushEnabled, ...rest } = overrides
32 32
return prisma.user.create({
33 33
data: {
34 34
email: rest.email ?? 'test@example.com',
35 35
firstName: rest.firstName ?? 'Test',
36 36
lastName: rest.lastName ?? 'User',
37 37
encryptedPassword: await bcrypt.hash(password ?? 'password123', 10),
38 38
...(pushToken !== undefined && { pushToken }),
39 -
...(notificationsEnabled !== undefined && { notificationsEnabled }),
39 +
...(pushEnabled !== undefined && { pushEnabled }),
40 40
},
41 41
})
42 42
}
43 43
44 44
// Fields that must never leave the API. Asserting absence at every
57 57
expect(user).toBeDefined()
58 58
for (const field of SECRET_FIELDS) {
59 59
expect(user).not.toHaveProperty(field)
60 60
}
61 61
}
api/tests/users/preferences.test.ts
1 1
import { describe, it, expect, afterAll, beforeEach } from 'vitest'
2 2
import request from 'supertest'
3 3
import { createApp } from '@app/createApp'
4 4
import { DailyReminderJobName } from '@app/lib/queue/types'
5 5
import {
28 28
const token = generateTestToken(user.id, user.email)
29 29
30 30
const res = await request(app)
31 31
.patch('/users/me')
32 32
.set('Authorization', `Bearer ${token}`)
33 -
.send({ user: { notificationsEnabled: true, dailyReminderTime: '08:30' } })
33 +
.send({ user: { pushEnabled: true, dailyReminderTime: '08:30' } })
34 34
35 35
expect(res.status).toBe(200)
36 -
expect(res.body.user.notificationsEnabled).toBe(true)
36 +
expect(res.body.user.pushEnabled).toBe(true)
37 37
expect(res.body.user.dailyReminderTime).toBe('08:30')
38 38
expectPublicUser(res.body.user)
39 39
40 40
const dbUser = await db.user.findUnique({ where: { id: user.id } })
41 41
expect(dbUser!.dailyReminderTime).toBe('08:30')
42 +
43 +
expect(recordedJobs).toContainEqual(
44 +
expect.objectContaining({
45 +
queue: 'daily-reminders',
46 +
name: DailyReminderJobName.Send,
47 +
data: { userId: user.id, scheduledTime: '08:30' },
48 +
})
49 +
)
50 +
})
51 +
52 +
it('schedules the daily reminder even when push is disabled', async () => {
53 +
const user = await createTestUser(db, {
54 +
email: 'nopush@example.com',
55 +
pushEnabled: false,
56 +
})
57 +
const token = generateTestToken(user.id, user.email)
58 +
59 +
const res = await request(app)
60 +
.patch('/users/me')
61 +
.set('Authorization', `Bearer ${token}`)
62 +
.send({ user: { dailyReminderTime: '08:30' } })
63 +
64 +
expect(res.status).toBe(200)
42 65
43 66
expect(recordedJobs).toContainEqual(
44 67
expect.objectContaining({
45 68
queue: 'daily-reminders',
46 69
name: DailyReminderJobName.Send,
103 126
104 127
expect(res.status).toBe(400)
105 128
expect(res.body.fields).toBeTruthy()
106 129
})
107 130
108 -
it('returns 400 for a non-boolean notificationsEnabled', async () => {
131 +
it('returns 400 for a non-boolean pushEnabled', async () => {
109 132
const user = await createTestUser(db, { email: 'prefs@example.com' })
110 133
const token = generateTestToken(user.id, user.email)
111 134
112 135
const res = await request(app)
113 136
.patch('/users/me')
114 137
.set('Authorization', `Bearer ${token}`)
115 -
.send({ user: { notificationsEnabled: 'yes' } })
138 +
.send({ user: { pushEnabled: 'yes' } })
116 139
117 140
expect(res.status).toBe(400)
118 141
})
119 142
120 143
it('returns 401 without a token', async () => {
121 144
const res = await request(app)
122 145
.patch('/users/me')
123 -
.send({ user: { notificationsEnabled: false } })
146 +
.send({ user: { pushEnabled: false } })
124 147
125 148
expect(res.status).toBe(401)
126 149
})
127 150
})
frontend/app/(tabs)/settings.tsx
1 1
import {
2 2
TouchableOpacity,
3 3
StyleSheet,
4 4
View,
5 5
Switch,
26 26
const [logoutVisible, setLogoutVisible] = useState(false);
27 27
const [exportVisible, setExportVisible] = useState(false);
28 28
const [exporting, setExporting] = useState(false);
29 29
const { showToast } = useToast();
30 30
31 -
const notificationsEnabled = user?.notificationsEnabled ?? true;
32 -
const dailyReminderTime = user?.dailyReminderTime ?? "20:00";
31 +
const pushEnabled = user?.pushEnabled ?? true;
32 +
const dailyReminderTime = user?.dailyReminderTime ?? null;
33 +
const reminderEnabled = dailyReminderTime !== null;
33 34
const weeklyResumeEnabled = user?.weeklyResumeEnabled ?? false;
35 +
const [lastReminderTime, setLastReminderTime] = useState("20:00");
36 +
const reminderTime = dailyReminderTime ?? lastReminderTime;
34 37
35 38
const patchUserMe = usePatchUserMe((updatedUser) => {
36 39
updateAuthUser(updatedUser);
37 40
});
38 41
39 -
const handleNotificationToggle = (value: boolean) => {
40 -
patchUserMe.mutate({ notificationsEnabled: value });
42 +
const handlePushToggle = (value: boolean) => {
43 +
patchUserMe.mutate({ pushEnabled: value });
44 +
};
45 +
46 +
const handleReminderToggle = (value: boolean) => {
47 +
if (dailyReminderTime) {
48 +
setLastReminderTime(dailyReminderTime);
49 +
}
50 +
51 +
patchUserMe.mutate({ dailyReminderTime: value ? reminderTime : null });
41 52
};
42 53
43 54
const handleWeeklyResumeToggle = (value: boolean) => {
44 55
patchUserMe.mutate({ weeklyResumeEnabled: value });
45 56
};
46 57
47 58
const handleTimeChange = (date: Date) => {
48 59
const time = `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`;
60 +
setLastReminderTime(time);
49 61
patchUserMe.mutate({ dailyReminderTime: time });
50 62
};
51 63
52 64
const textColor = useThemeColor({}, "text");
53 65
149 161
justifyContent: "space-between",
150 162
marginLeft: 12,
151 163
flex: 1,
152 164
}}
153 165
>
166 +
<Text style={styles.rowLabel}>Notificações por push</Text>
167 +
<Text style={styles.rowSubtitle}>
168 +
Os lembretes continuam no app mesmo com o push desativado.
169 +
</Text>
170 +
</View>
171 +
172 +
<Switch
173 +
onValueChange={handlePushToggle}
174 +
thumbColor={Colors.light.tint}
175 +
trackColor={{
176 +
false: Colors.light.disabled,
177 +
true: Colors.light.gray,
178 +
}}
179 +
value={pushEnabled}
180 +
disabled={patchUserMe.isPending}
181 +
/>
182 +
</View>
183 +
184 +
<View style={styles.divider} />
185 +
186 +
<View
187 +
style={{
188 +
flexDirection: "row",
189 +
alignItems: "center",
190 +
justifyContent: "space-between",
191 +
}}
192 +
>
193 +
<View
194 +
style={[
195 +
styles.cardIcon,
196 +
{ backgroundColor: Colors.light.accentIndigo, marginBottom: 5 },
197 +
]}
198 +
>
199 +
<Ionicons
200 +
name="alarm-outline"
201 +
size={20}
202 +
color={Colors.light.black}
203 +
/>
204 +
</View>
205 +
206 +
<View
207 +
style={{
208 +
flexDirection: "column",
209 +
alignItems: "flex-start",
210 +
justifyContent: "space-between",
211 +
marginLeft: 12,
212 +
flex: 1,
213 +
}}
214 +
>
154 215
<Text style={styles.rowLabel}>Lembrete diário de humor</Text>
155 216
<Text style={styles.rowSubtitle}>
156 -
{notificationsEnabled ? `Diário às ${dailyReminderTime}` : "Desativado"}
217 +
{reminderEnabled ? `Diário às ${reminderTime}` : "Desativado"}
157 218
</Text>
158 219
</View>
159 220
160 221
<Switch
161 -
onValueChange={handleNotificationToggle}
222 +
onValueChange={handleReminderToggle}
162 223
thumbColor={Colors.light.tint}
163 224
trackColor={{
164 225
false: Colors.light.disabled,
165 226
true: Colors.light.gray,
166 227
}}
167 -
value={notificationsEnabled}
228 +
value={reminderEnabled}
168 229
disabled={patchUserMe.isPending}
169 230
/>
170 231
</View>
171 232
172 -
{notificationsEnabled && (
233 +
{reminderEnabled && (
173 234
<TimePicker
174 235
value={(() => {
175 -
const [h, m] = dailyReminderTime.split(":").map(Number);
236 +
const [h, m] = reminderTime.split(":").map(Number);
176 237
const d = new Date();
177 238
d.setHours(h, m, 0, 0);
178 239
return d;
179 240
})()}
180 241
onChange={handleTimeChange}
402 463
height: StyleSheet.hairlineWidth,
403 464
backgroundColor: "#D1D5DB",
404 465
marginVertical: 16,
405 466
},
406 467
});
frontend/lib/api/types.ts
1 1
export interface User {
2 2
id: number;
3 3
email: string;
4 4
firstName: string;
5 5
lastName?: string;
6 6
updatedAt: Date;
7 7
avatarKey?: string;
8 8
avatarURL?: string;
9 9
active: boolean;
10 -
notificationsEnabled?: boolean;
10 +
pushEnabled?: boolean;
11 11
dailyReminderTime?: string;
12 12
weeklyResumeEnabled?: boolean;
13 13
onboardingSeenAt?: Date | null;
14 14
}
15 15
62 62
export interface UserUpdateResponse {
63 63
user: User;
64 64
}
65 65
66 66
export interface UserPreferencesPayload {
67 -
notificationsEnabled?: boolean;
67 +
pushEnabled?: boolean;
68 68
dailyReminderTime?: string | null;
69 69
weeklyResumeEnabled?: boolean;
70 70
}
71 71
72 72
export interface ExportDataResponse {
390 390
moodId?: number;
391 391
}
392 392
export interface MedicineRegimenResponse { regimen: MedicineRegimen; }
393 393
394 394
export interface LinkMoodPayload { moodId: number; perceivedImpact: number; }