eletrotupi / tcc/ commit / 30f98b8

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 files
Parents dafa6f8
Changed files
15
Additions
+127
Deletions
-43
/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; }