/** * Unified Notification Service * * Provides a platform-agnostic interface for managing notifications across * web and native platforms. Automatically delegates to the appropriate * implementation based on the runtime platform: * * - Native platforms (iOS/Android): Uses DailyNotificationPlugin * - Web/PWA: Uses Web Push with service workers and VAPID * * @author Matthew Raymer * @version 1.0.0 * @since 2026-01-21 */ import { Capacitor } from "@capacitor/core"; import { logger } from "@/utils/logger"; import { getOrCreateDeviceId } from "./deviceId"; import { getNotificationApiBaseUrl, getTestMode, } from "./NotificationDebugConfig"; import { getNotificationApiHeaders, httpAuthErrorMessage, logNotificationAuthFailure, } from "./notificationApiAuth"; import { logTokenRegistrationFailure, logTokenRegistrationStarted, logTokenRegistrationSuccess, } from "./notificationLog"; import { NativeNotificationService } from "./NativeNotificationService"; import { WebPushNotificationService } from "./WebPushNotificationService"; /** * Registers an FCM device token with the app backend (native Capacitor token or web getToken). */ export async function registerToken(fcmToken: string): Promise { logTokenRegistrationStarted(fcmToken); const deviceId = await getOrCreateDeviceId(); const baseUrl = getNotificationApiBaseUrl(); try { const auth = await getNotificationApiHeaders("register"); if (!auth.ok) { logNotificationAuthFailure("register", auth.message); throw new Error(`registerToken auth unavailable: ${auth.message}`); } const res = await fetch(`${baseUrl}/notifications/register`, { method: "POST", headers: auth.headers, body: JSON.stringify({ deviceId, fcmToken, platform: Capacitor.getPlatform(), testMode: getTestMode(), }), }); if (!res.ok) { const authDetail = res.status === 401 || res.status === 403 ? httpAuthErrorMessage(res.status) : `HTTP ${res.status}`; logger.warn("[NotificationService] registerToken failed", { status: res.status, statusText: res.statusText, authDetail, }); throw new Error(`registerToken failed: ${authDetail}`); } logTokenRegistrationSuccess(fcmToken); } catch (err) { logTokenRegistrationFailure(fcmToken, err); throw err; } } /** * Options for scheduling a daily notification */ export interface DailyNotificationOptions { /** * Time to send notification in HH:mm format (24-hour) * Example: "09:00" for 9 AM, "17:30" for 5:30 PM */ time: string; /** * Notification title */ title: string; /** * Notification body/message */ body: string; /** * Optional notification priority * @default 'normal' */ priority?: "low" | "normal" | "high"; /** * Optional rollover interval in minutes (e.g. 10 for testing). When set, next occurrence * is scheduled this many minutes after the current trigger instead of 24 hours. * Plugin must support and persist this for it to take effect after reboot. */ rolloverIntervalMinutes?: number; } /** * Current notification status */ export interface NotificationStatus { /** * Whether notifications are currently enabled */ enabled: boolean; /** * Scheduled time in HH:mm format, if any */ scheduledTime?: string; /** * Current notification message, if any */ message?: string; /** * Platform-specific notification type identifier */ notificationType?: string; } /** * Permission status result */ export interface PermissionStatus { /** * Whether notification permissions are granted */ granted: boolean; /** * Additional platform-specific permission details */ details?: { notifications?: boolean; exactAlarm?: boolean; backgroundRefresh?: boolean; }; } /** * Unified notification service interface * All platform-specific implementations must conform to this interface */ export interface NotificationServiceInterface { /** * Check if notifications are supported on current platform * @returns true if notifications are supported */ isSupported(): boolean; /** * Request notification permissions from the user * Shows system permission dialog on first call * @returns Promise that resolves to true if permissions granted */ requestPermissions(): Promise; /** * Check current notification permission status * @returns Promise with permission status */ checkPermissions(): Promise; /** * Schedule a daily notification/reminder * @param options Notification configuration * @returns Promise that resolves to true if scheduling succeeded */ scheduleDailyNotification( options: DailyNotificationOptions, ): Promise; /** * Cancel all daily notifications * @returns Promise that resolves when cancellation is complete */ cancelDailyNotification(): Promise; /** * Get current notification status * @returns Promise with current notification state */ getStatus(): Promise; /** * Get the platform name for this service * @returns Platform identifier (e.g., 'native', 'web') */ getPlatformName(): string; } /** * Notification Service Factory * * Singleton factory that creates and manages the appropriate notification * service implementation based on the current platform. * * @example * ```typescript * const notificationService = NotificationService.getInstance(); * const granted = await notificationService.requestPermissions(); * if (granted) { * await notificationService.scheduleDailyNotification({ * time: '09:00', * title: 'Daily Check-In', * body: 'Time to check your TimeSafari activity' * }); * } * ``` */ export class NotificationService { private static instance: NotificationServiceInterface | null = null; private static instanceCreated = false; /** * Get the singleton notification service instance * Creates the appropriate platform-specific implementation on first call * * @returns NotificationServiceInterface implementation for current platform */ static getInstance(): NotificationServiceInterface { if (!this.instance) { const platform = Capacitor.getPlatform(); if (!this.instanceCreated) { // Log initialization for debugging (only happens once per app lifecycle) // eslint-disable-next-line no-console console.log( `[NotificationService] Creating ${this.isNative() ? "native" : "web"} notification service for platform: ${platform}`, ); this.instanceCreated = true; } if (this.isNative()) { // iOS/Android: Use native plugin this.instance = new NativeNotificationService(); } else { // Web/PWA: Use web push this.instance = new WebPushNotificationService(); } } return this.instance; } /** * Check if running on a native platform (iOS/Android) * @returns true if running on iOS or Android */ static isNative(): boolean { return Capacitor.isNativePlatform(); } /** * Get the current platform name * @returns Platform identifier: 'ios', 'android', 'web', 'electron' */ static getPlatform(): string { return Capacitor.getPlatform(); } /** * Reset the singleton instance (for testing purposes) * @internal */ static reset(): void { this.instance = null; this.instanceCreated = false; } }