130 lines
3.4 KiB
TypeScript
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));
|
|
}
|