summaryrefslogtreecommitdiff
path: root/lib/notifications.ts
diff options
context:
space:
mode:
authorJustZvan <justzvan@justzvan.xyz>2026-02-06 13:22:33 +0100
committerJustZvan <justzvan@justzvan.xyz>2026-02-06 13:22:33 +0100
commit7eb8ccae48b0cc18a9dcaa9c3626a02df8e6d919 (patch)
tree57b7dd06ac9aa7053c671d916f7183e3b4fa9410 /lib/notifications.ts
feat: initial commit!
Diffstat (limited to 'lib/notifications.ts')
-rw-r--r--lib/notifications.ts206
1 files changed, 206 insertions, 0 deletions
diff --git a/lib/notifications.ts b/lib/notifications.ts
new file mode 100644
index 0000000..a9c9a95
--- /dev/null
+++ b/lib/notifications.ts
@@ -0,0 +1,206 @@
+import Constants from "expo-constants";
+import * as Device from "expo-device";
+import * as Notifications from "expo-notifications";
+import { Platform } from "react-native";
+import { apiClient } from "../api/client";
+
+// Configure notification handling behavior
+Notifications.setNotificationHandler({
+ handleNotification: async () => ({
+ shouldShowAlert: true,
+ shouldPlaySound: true,
+ shouldSetBadge: true,
+ shouldShowBanner: true,
+ shouldShowList: true,
+ }),
+});
+
+export type NotificationPermissionStatus =
+ | "granted"
+ | "denied"
+ | "undetermined";
+
+/**
+ * Get current notification permission status
+ */
+export async function getNotificationPermissionStatus(): Promise<NotificationPermissionStatus> {
+ const { status } = await Notifications.getPermissionsAsync();
+ return status;
+}
+
+/**
+ * Request notification permissions from the user
+ */
+export async function requestNotificationPermissions(): Promise<NotificationPermissionStatus> {
+ // Check if we're on a physical device (notifications don't work on simulator)
+ if (!Device.isDevice) {
+ console.log("Push notifications require a physical device");
+ return "denied";
+ }
+
+ // Get existing permissions first
+ const { status: existingStatus } = await Notifications.getPermissionsAsync();
+
+ let finalStatus = existingStatus;
+
+ // Only ask if permissions have not already been determined
+ if (existingStatus !== "granted") {
+ const { status } = await Notifications.requestPermissionsAsync();
+ finalStatus = status;
+ }
+
+ return finalStatus;
+}
+
+/**
+ * Get the Expo push token for this device
+ */
+export async function getExpoPushToken(): Promise<string | null> {
+ if (!Device.isDevice) {
+ console.log("Push notifications require a physical device");
+ return null;
+ }
+
+ const { status } = await Notifications.getPermissionsAsync();
+ if (status !== "granted") {
+ console.log("Notification permissions not granted");
+ return null;
+ }
+
+ try {
+ // Get the project ID from Expo config
+ const projectId =
+ Constants.expoConfig?.extra?.eas?.projectId ??
+ Constants.easConfig?.projectId;
+
+ if (!projectId) {
+ console.error("Project ID not found in Expo config");
+ return null;
+ }
+
+ const tokenData = await Notifications.getExpoPushTokenAsync({
+ projectId,
+ });
+
+ return tokenData.data;
+ } catch (error) {
+ console.error("Failed to get Expo push token:", error);
+ return null;
+ }
+}
+
+/**
+ * Register push token with the backend
+ */
+export async function registerPushToken(): Promise<boolean> {
+ const token = await getExpoPushToken();
+
+ if (!token) {
+ console.log("No push token available to register");
+ return false;
+ }
+
+ try {
+ await apiClient.post("/parent/push-token", { token });
+ console.log("Push token registered successfully");
+ return true;
+ } catch (error) {
+ console.error("Failed to register push token:", error);
+ return false;
+ }
+}
+
+/**
+ * Remove push token from the backend
+ */
+export async function unregisterPushToken(): Promise<boolean> {
+ const token = await getExpoPushToken();
+
+ if (!token) {
+ console.log("No push token available to unregister");
+ return false;
+ }
+
+ try {
+ await apiClient.delete("/parent/push-token", { token });
+ console.log("Push token unregistered successfully");
+ return true;
+ } catch (error) {
+ console.error("Failed to unregister push token:", error);
+ return false;
+ }
+}
+
+/**
+ * Set up Android notification channel for alerts
+ */
+export async function setupNotificationChannels(): Promise<void> {
+ if (Platform.OS === "android") {
+ await Notifications.setNotificationChannelAsync("default", {
+ name: "Default",
+ importance: Notifications.AndroidImportance.DEFAULT,
+ vibrationPattern: [0, 250, 250, 250],
+ lightColor: "#FF231F7C",
+ });
+
+ await Notifications.setNotificationChannelAsync("alerts", {
+ name: "Safety Alerts",
+ description: "Important safety alerts about your child's device",
+ importance: Notifications.AndroidImportance.HIGH,
+ vibrationPattern: [0, 500, 250, 500],
+ lightColor: "#FF0000",
+ sound: "default",
+ enableVibrate: true,
+ enableLights: true,
+ });
+ }
+}
+
+/**
+ * Add a listener for received notifications (when app is foregrounded)
+ */
+export function addNotificationReceivedListener(
+ callback: (notification: Notifications.Notification) => void
+): Notifications.EventSubscription {
+ return Notifications.addNotificationReceivedListener(callback);
+}
+
+/**
+ * Add a listener for notification responses (when user taps notification)
+ */
+export function addNotificationResponseListener(
+ callback: (response: Notifications.NotificationResponse) => void
+): Notifications.EventSubscription {
+ return Notifications.addNotificationResponseReceivedListener(callback);
+}
+
+/**
+ * Get the last notification response (for handling cold start from notification)
+ */
+export async function getLastNotificationResponse(): Promise<Notifications.NotificationResponse | null> {
+ return Notifications.getLastNotificationResponseAsync();
+}
+
+/**
+ * Initialize notifications: setup channels, request permissions, and register token
+ * Call this when the user is authenticated
+ */
+export async function initializeNotifications(): Promise<{
+ permissionStatus: NotificationPermissionStatus;
+ tokenRegistered: boolean;
+}> {
+ // Setup Android channels first
+ await setupNotificationChannels();
+
+ // Request permissions
+ const permissionStatus = await requestNotificationPermissions();
+
+ if (permissionStatus !== "granted") {
+ return { permissionStatus, tokenRegistered: false };
+ }
+
+ // Register token with backend
+ const tokenRegistered = await registerPushToken();
+
+ return { permissionStatus, tokenRegistered };
+}