Files
birthday/apk/utils/birthday-notifications.ts

130 lines
3.4 KiB
TypeScript

import { Platform } from "react-native";
import * as Notifications from "expo-notifications";
import type { BirthdayEntry } from "@/context/birthdays-context";
const CHANNEL_ID = "birthdays";
const NOTIFICATION_HOUR = 9;
const NOTIFICATION_MINUTE = 0;
const IDENTIFIER_PREFIX = "birthday-";
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldPlaySound: true,
shouldSetBadge: false,
shouldShowBanner: true,
shouldShowList: true,
}),
});
function getNotificationIdentifier(id: string) {
return `${IDENTIFIER_PREFIX}${id}`;
}
function parseBirthdayDate(date: string) {
return new Date(`${date}T00:00:00`);
}
async function configureAndroidChannel() {
if (Platform.OS !== "android") {
return;
}
await Notifications.setNotificationChannelAsync(CHANNEL_ID, {
name: "Birthdays",
description: "Birthday reminders",
importance: Notifications.AndroidImportance.DEFAULT,
});
}
async function hasNotificationPermissionAsync() {
const settings = await Notifications.getPermissionsAsync();
return (
settings.granted ||
settings.ios?.status === Notifications.IosAuthorizationStatus.PROVISIONAL
);
}
async function ensureNotificationPermissionAsync() {
await configureAndroidChannel();
if (await hasNotificationPermissionAsync()) {
return true;
}
const settings = await Notifications.requestPermissionsAsync({
ios: {
allowAlert: true,
allowBadge: false,
allowSound: true,
},
});
return (
settings.granted ||
settings.ios?.status === Notifications.IosAuthorizationStatus.PROVISIONAL
);
}
async function clearStaleBirthdayNotifications(birthdays: BirthdayEntry[]) {
const validIdentifiers = new Set(
birthdays.map((birthday) => getNotificationIdentifier(birthday.id))
);
const scheduledNotifications =
await Notifications.getAllScheduledNotificationsAsync();
await Promise.all(
scheduledNotifications
.filter(
(notification) =>
notification.identifier.startsWith(IDENTIFIER_PREFIX) &&
!validIdentifiers.has(notification.identifier)
)
.map((notification) =>
Notifications.cancelScheduledNotificationAsync(notification.identifier)
)
);
}
async function scheduleBirthdayNotification(birthday: BirthdayEntry) {
const parsedDate = parseBirthdayDate(birthday.date);
const month = parsedDate.getMonth();
const day = parsedDate.getDate();
await Notifications.cancelScheduledNotificationAsync(
getNotificationIdentifier(birthday.id)
);
await Notifications.scheduleNotificationAsync({
identifier: getNotificationIdentifier(birthday.id),
content: {
title: `Today is ${birthday.name}'s birthday`,
body: `Wish ${birthday.name} a happy birthday!`,
sound: true,
},
trigger: {
type: Notifications.SchedulableTriggerInputTypes.YEARLY,
channelId: Platform.OS === "android" ? CHANNEL_ID : undefined,
month,
day,
hour: NOTIFICATION_HOUR,
minute: NOTIFICATION_MINUTE,
},
});
}
export async function syncBirthdayNotifications(birthdays: BirthdayEntry[]) {
if (Platform.OS === "web") {
return;
}
const permissionGranted = await ensureNotificationPermissionAsync();
if (!permissionGranted) {
return;
}
await clearStaleBirthdayNotifications(birthdays);
await Promise.all(birthdays.map(scheduleBirthdayNotification));
}