From 7eb8ccae48b0cc18a9dcaa9c3626a02df8e6d919 Mon Sep 17 00:00:00 2001 From: JustZvan Date: Fri, 6 Feb 2026 13:22:33 +0100 Subject: feat: initial commit! --- lib/notifications.ts | 206 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 lib/notifications.ts (limited to 'lib/notifications.ts') 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 { + const { status } = await Notifications.getPermissionsAsync(); + return status; +} + +/** + * Request notification permissions from the user + */ +export async function requestNotificationPermissions(): Promise { + // 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 { + 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 { + 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 { + 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 { + 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 { + 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 }; +} -- cgit v1.2.3