UNPKG

notification-kit

Version:

A unified notification library for React + Capacitor apps. One API for push notifications, in-app notifications, and local notifications across Web, iOS, and Android.

1 lines 168 kB
{"version":3,"file":"inApp-DOHkBonY.mjs","sources":["../src/utils/logger.ts","../src/utils/dynamic-loader.ts","../src/utils/capacitor-types.ts","../src/core/NotificationKit.ts","../src/core/permissions.ts","../src/core/platform.ts","../src/core/storage.ts","../src/utils/scheduling.ts","../src/utils/validation.ts","../src/utils/formatting.ts","../src/utils/inApp.ts"],"sourcesContent":["/**\n * Simple logger utility for notification-kit\n */\nexport class Logger {\n private static isDebugEnabled = false\n\n static enableDebug(): void {\n Logger.isDebugEnabled = true\n }\n\n static disableDebug(): void {\n Logger.isDebugEnabled = false\n }\n\n static debug(...args: any[]): void {\n if (Logger.isDebugEnabled && typeof console !== 'undefined') {\n console.log('[notification-kit]', ...args)\n }\n }\n\n static info(...args: any[]): void {\n if (typeof console !== 'undefined') {\n console.info('[notification-kit]', ...args)\n }\n }\n\n static warn(...args: any[]): void {\n if (typeof console !== 'undefined') {\n console.warn('[notification-kit]', ...args)\n }\n }\n\n static error(...args: any[]): void {\n if (typeof console !== 'undefined') {\n console.error('[notification-kit]', ...args)\n }\n }\n}","import type { Platform } from '@/types'\nimport { Logger } from '@/utils/logger'\n\n/**\n * Dynamic dependency loader with runtime checks\n */\nexport class DynamicLoader {\n private static loadedModules = new Map<string, any>()\n private static loadingPromises = new Map<string, Promise<any>>()\n\n /**\n * Check if Capacitor is available\n */\n static isCapacitorAvailable(): boolean {\n try {\n return typeof window !== 'undefined' && 'Capacitor' in window\n } catch {\n return false\n }\n }\n\n /**\n * Load Capacitor Core dynamically\n */\n static async loadCapacitorCore(): Promise<typeof import('@capacitor/core') | null> {\n const cacheKey = '@capacitor/core'\n \n if (this.loadedModules.has(cacheKey)) {\n return this.loadedModules.get(cacheKey)\n }\n\n if (this.loadingPromises.has(cacheKey)) {\n return this.loadingPromises.get(cacheKey)\n }\n\n const loadPromise = (async () => {\n try {\n if (!this.isCapacitorAvailable()) {\n return null\n }\n const module = await import('@capacitor/core')\n this.loadedModules.set(cacheKey, module)\n return module\n } catch (error) {\n Logger.warn('Capacitor Core not available. Some features may be limited.')\n return null\n }\n })()\n\n this.loadingPromises.set(cacheKey, loadPromise)\n return loadPromise\n }\n\n /**\n * Load Capacitor Push Notifications\n */\n static async loadPushNotifications(): Promise<typeof import('@capacitor/push-notifications') | null> {\n const cacheKey = '@capacitor/push-notifications'\n \n if (this.loadedModules.has(cacheKey)) {\n return this.loadedModules.get(cacheKey)\n }\n\n if (this.loadingPromises.has(cacheKey)) {\n return this.loadingPromises.get(cacheKey)\n }\n\n const loadPromise = (async () => {\n try {\n const module = await import('@capacitor/push-notifications')\n this.loadedModules.set(cacheKey, module)\n return module\n } catch (error) {\n throw new Error(\n 'Push notifications require @capacitor/push-notifications. ' +\n 'Please install it: yarn add @capacitor/push-notifications'\n )\n }\n })()\n\n this.loadingPromises.set(cacheKey, loadPromise)\n return loadPromise\n }\n\n /**\n * Load Capacitor Local Notifications\n */\n static async loadLocalNotifications(): Promise<typeof import('@capacitor/local-notifications') | null> {\n const cacheKey = '@capacitor/local-notifications'\n \n if (this.loadedModules.has(cacheKey)) {\n return this.loadedModules.get(cacheKey)\n }\n\n if (this.loadingPromises.has(cacheKey)) {\n return this.loadingPromises.get(cacheKey)\n }\n\n const loadPromise = (async () => {\n try {\n const module = await import('@capacitor/local-notifications')\n this.loadedModules.set(cacheKey, module)\n return module\n } catch (error) {\n throw new Error(\n 'Local notifications require @capacitor/local-notifications. ' +\n 'Please install it: yarn add @capacitor/local-notifications'\n )\n }\n })()\n\n this.loadingPromises.set(cacheKey, loadPromise)\n return loadPromise\n }\n\n /**\n * Load Capacitor Preferences\n */\n static async loadPreferences(): Promise<typeof import('@capacitor/preferences') | null> {\n const cacheKey = '@capacitor/preferences'\n \n if (this.loadedModules.has(cacheKey)) {\n return this.loadedModules.get(cacheKey)\n }\n\n if (this.loadingPromises.has(cacheKey)) {\n return this.loadingPromises.get(cacheKey)\n }\n\n const loadPromise = (async () => {\n try {\n const module = await import('@capacitor/preferences')\n this.loadedModules.set(cacheKey, module)\n return module\n } catch (error) {\n // Preferences are optional, fallback to localStorage\n return null\n }\n })()\n\n this.loadingPromises.set(cacheKey, loadPromise)\n return loadPromise\n }\n\n /**\n * Load Firebase\n */\n static async loadFirebase(): Promise<typeof import('firebase/app') | null> {\n const cacheKey = 'firebase/app'\n \n if (this.loadedModules.has(cacheKey)) {\n return this.loadedModules.get(cacheKey)\n }\n\n if (this.loadingPromises.has(cacheKey)) {\n return this.loadingPromises.get(cacheKey)\n }\n\n const loadPromise = (async () => {\n try {\n const module = await import('firebase/app')\n this.loadedModules.set(cacheKey, module)\n return module\n } catch (error) {\n throw new Error(\n 'Firebase provider requires firebase. ' +\n 'Please install it: yarn add firebase'\n )\n }\n })()\n\n this.loadingPromises.set(cacheKey, loadPromise)\n return loadPromise\n }\n\n /**\n * Load Firebase Messaging\n */\n static async loadFirebaseMessaging(): Promise<typeof import('firebase/messaging') | null> {\n const cacheKey = 'firebase/messaging'\n \n if (this.loadedModules.has(cacheKey)) {\n return this.loadedModules.get(cacheKey)\n }\n\n if (this.loadingPromises.has(cacheKey)) {\n return this.loadingPromises.get(cacheKey)\n }\n\n const loadPromise = (async () => {\n try {\n const module = await import('firebase/messaging')\n this.loadedModules.set(cacheKey, module)\n return module\n } catch (error) {\n throw new Error(\n 'Firebase messaging requires firebase. ' +\n 'Please install it: yarn add firebase'\n )\n }\n })()\n\n this.loadingPromises.set(cacheKey, loadPromise)\n return loadPromise\n }\n\n /**\n * Load OneSignal\n */\n static async loadOneSignal(): Promise<typeof import('react-onesignal') | null> {\n const cacheKey = 'react-onesignal'\n \n if (this.loadedModules.has(cacheKey)) {\n return this.loadedModules.get(cacheKey)\n }\n\n if (this.loadingPromises.has(cacheKey)) {\n return this.loadingPromises.get(cacheKey)\n }\n\n const loadPromise = (async () => {\n try {\n const module = await import('react-onesignal')\n this.loadedModules.set(cacheKey, module)\n return module\n } catch (error) {\n throw new Error(\n 'OneSignal provider requires react-onesignal. ' +\n 'Please install it: yarn add react-onesignal'\n )\n }\n })()\n\n this.loadingPromises.set(cacheKey, loadPromise)\n return loadPromise\n }\n\n /**\n * Get current platform without Capacitor\n */\n static async getPlatform(): Promise<Platform> {\n const capacitor = await this.loadCapacitorCore()\n \n if (capacitor && capacitor.Capacitor.isNativePlatform()) {\n return capacitor.Capacitor.getPlatform() as Platform\n } else if (typeof window !== 'undefined') {\n if (window.navigator.userAgent.includes('Electron')) {\n return 'electron'\n }\n return 'web'\n } else {\n return 'unknown'\n }\n }\n\n /**\n * Check if platform is native\n */\n static async isNativePlatform(): Promise<boolean> {\n const capacitor = await this.loadCapacitorCore()\n return capacitor ? capacitor.Capacitor.isNativePlatform() : false\n }\n\n /**\n * Clear module cache\n */\n static clearCache(): void {\n this.loadedModules.clear()\n this.loadingPromises.clear()\n }\n}","/**\n * Type conversion utilities for Capacitor compatibility\n */\n\nimport type {\n LocalNotificationSchema,\n Channel as CapacitorChannel,\n Importance as CapacitorImportance,\n} from '@capacitor/local-notifications'\nimport type {\n ScheduleOptions,\n NotificationChannel,\n ChannelImportance,\n LocalNotificationPayload,\n} from '@/types'\n\n/**\n * Convert our ChannelImportance to Capacitor's Importance\n */\nexport function toCapacitorImportance(\n importance: ChannelImportance\n): CapacitorImportance {\n const map: Record<ChannelImportance, CapacitorImportance> = {\n none: 1,\n min: 2,\n low: 2,\n default: 3,\n high: 4,\n max: 5,\n }\n return map[importance] || 3\n}\n\n/**\n * Convert Capacitor's Importance to our ChannelImportance\n */\nexport function fromCapacitorImportance(\n importance: CapacitorImportance\n): ChannelImportance {\n const map: Record<CapacitorImportance, ChannelImportance> = {\n 1: 'none',\n 2: 'low',\n 3: 'default',\n 4: 'high',\n 5: 'max',\n }\n return map[importance] || 'default'\n}\n\n/**\n * Convert our NotificationChannel to Capacitor's Channel\n */\nexport function toCapacitorChannel(\n channel: NotificationChannel\n): CapacitorChannel {\n const capacitorChannel: any = {\n id: channel.id,\n name: channel.name,\n description: channel.description ?? '',\n importance: channel.importance\n ? toCapacitorImportance(channel.importance)\n : undefined,\n visibility: channel.visibility as any, // Capacitor uses similar values\n }\n\n if (channel.sound !== undefined) {\n capacitorChannel.sound = channel.sound\n }\n\n if (channel.vibration !== undefined) {\n capacitorChannel.vibration = channel.vibration\n }\n\n if (channel.lights !== undefined) {\n capacitorChannel.lights = channel.lights\n }\n\n return capacitorChannel as CapacitorChannel\n}\n\n/**\n * Convert Capacitor's Channel to our NotificationChannel\n */\nexport function fromCapacitorChannel(\n channel: CapacitorChannel\n): NotificationChannel {\n const notificationChannel: any = {\n id: channel.id,\n name: channel.name,\n description: channel.description ?? '',\n importance: channel.importance\n ? fromCapacitorImportance(channel.importance)\n : 'default',\n visibility: channel.visibility as any,\n }\n\n if (channel.sound !== undefined) {\n notificationChannel.sound = channel.sound\n }\n\n if (channel.vibration !== undefined) {\n notificationChannel.vibration = channel.vibration\n }\n\n if (channel.lights !== undefined) {\n notificationChannel.lights = channel.lights\n }\n\n return notificationChannel as NotificationChannel\n}\n\n/**\n * Convert our ScheduleOptions to Capacitor's LocalNotificationSchema\n */\nexport function toCapacitorLocalNotification(\n options: ScheduleOptions & LocalNotificationPayload\n): LocalNotificationSchema {\n const notification: any = {\n id:\n typeof options.id === 'string'\n ? parseInt(options.id, 10)\n : options.id || Date.now(),\n title: options.title || '',\n body: options.body || '',\n largeBody: options.body,\n }\n\n if (options.summaryText !== undefined) {\n notification.summaryText = options.summaryText\n }\n\n if (options.at) {\n notification.schedule = { at: options.at }\n }\n\n if (options.sound !== undefined) {\n notification.sound = options.sound\n }\n\n if (options.smallIcon !== undefined) {\n notification.smallIcon = options.smallIcon\n }\n\n if (options.largeIcon !== undefined) {\n notification.largeIcon = options.largeIcon\n }\n\n if (options.color !== undefined) {\n notification.iconColor = options.color\n }\n\n if (options.channelId !== undefined) {\n notification.channelId = options.channelId\n }\n\n if (options.ongoing !== undefined) {\n notification.ongoing = options.ongoing\n }\n\n if (options.autoCancel !== undefined) {\n notification.autoCancel = options.autoCancel\n }\n\n if (options.group !== undefined) {\n notification.group = options.group\n }\n\n if (options.groupSummary !== undefined) {\n notification.groupSummary = options.groupSummary\n }\n\n if (options.extra || options.data) {\n notification.extra = options.extra || options.data\n }\n\n return notification as LocalNotificationSchema\n}\n\n/**\n * Convert Capacitor's LocalNotificationSchema to our types\n */\nexport function fromCapacitorLocalNotification(\n notification: LocalNotificationSchema\n): LocalNotificationPayload {\n const payload: any = {\n id: notification.id.toString(),\n title: notification.title,\n body: notification.body,\n }\n\n if (notification.summaryText !== undefined) {\n payload.summaryText = notification.summaryText\n }\n\n if (notification.sound !== undefined) {\n payload.sound = notification.sound\n }\n\n if (notification.smallIcon !== undefined) {\n payload.smallIcon = notification.smallIcon\n }\n\n if (notification.largeIcon !== undefined) {\n payload.largeIcon = notification.largeIcon\n }\n\n if (notification.iconColor !== undefined) {\n payload.color = notification.iconColor\n }\n\n if (notification.channelId !== undefined) {\n payload.channelId = notification.channelId\n }\n\n if (notification.ongoing !== undefined) {\n payload.ongoing = notification.ongoing\n }\n\n if (notification.autoCancel !== undefined) {\n payload.autoCancel = notification.autoCancel\n }\n\n if (notification.group !== undefined) {\n payload.group = notification.group\n }\n\n if (notification.groupSummary !== undefined) {\n payload.groupSummary = notification.groupSummary\n }\n\n if (notification.extra !== undefined) {\n payload.extra = notification.extra\n payload.data = notification.extra\n }\n\n return payload as LocalNotificationPayload\n}\n\n/**\n * Convert ProviderCapabilities to PlatformCapabilities\n */\nexport function toPlatformCapabilities(providerCaps: any): any {\n // For now, we'll create a basic mapping. In a real implementation,\n // this would map provider-specific capabilities to platform capabilities\n return {\n pushNotifications: providerCaps.pushNotifications || false,\n localNotifications: true, // Always true if Capacitor is available\n inAppNotifications: true, // Always true\n notificationChannels: providerCaps.channels || false,\n notificationActions: providerCaps.actions || false,\n notificationBadging: providerCaps.badges || false,\n notificationSound: providerCaps.sounds || false,\n notificationVibration: providerCaps.vibration || false,\n notificationLights: providerCaps.lights || false,\n notificationGrouping: providerCaps.groups || false,\n notificationImportance: providerCaps.channels || false,\n notificationVisibility: providerCaps.channels || false,\n notificationLockScreen: providerCaps.channels || false,\n notificationFullScreen: false,\n notificationHeadsUp: providerCaps.channels || false,\n notificationOngoing: providerCaps.channels || false,\n notificationProgress: providerCaps.progress || false,\n notificationBigText: providerCaps.bigText || false,\n notificationBigPicture: providerCaps.bigPicture || false,\n notificationInbox: providerCaps.inbox || false,\n notificationMedia: providerCaps.richMedia || false,\n notificationCustom: providerCaps.customData || false,\n notificationScheduling: providerCaps.scheduling || false,\n notificationGeofencing: providerCaps.geofencing || false,\n notificationTriggers: providerCaps.triggers || false,\n serviceWorker: providerCaps.webPush || false,\n webPushProtocol: providerCaps.webPush || false,\n backgroundSync: providerCaps.backgroundSync || false,\n foregroundService: false,\n criticalAlerts: false,\n provisionalAuth: false,\n appBadge: providerCaps.badges || false,\n quietHours: providerCaps.quietHours || false,\n doNotDisturb: false,\n }\n}\n","import { DynamicLoader } from '@/utils/dynamic-loader'\nimport type {\n NotificationConfig,\n NotificationProvider,\n Notification,\n PermissionStatus,\n ScheduleOptions,\n InAppOptions,\n NotificationChannel,\n PushNotificationPayload,\n LocalNotificationPayload,\n EventListener,\n NotificationEvent,\n NotificationEventCallback,\n NotificationEventMap,\n Platform,\n PlatformCapabilities,\n} from '@/types'\nimport {\n toCapacitorLocalNotification,\n fromCapacitorLocalNotification,\n toCapacitorChannel,\n fromCapacitorChannel,\n toPlatformCapabilities,\n} from '@/utils/capacitor-types'\n\n/**\n * Main NotificationKit class - provides unified API for all notification types\n */\nexport class NotificationKit {\n private static instance: NotificationKit | null = null\n private provider: NotificationProvider | null = null\n private config: NotificationConfig | null = null\n private initialized = false\n private eventListeners: Map<string, EventListener[]> = new Map()\n private platform: Platform = 'unknown'\n private capabilities: PlatformCapabilities | null = null\n\n private constructor() {\n // Platform detection will be done during init\n }\n\n /**\n * Get singleton instance\n */\n static getInstance(): NotificationKit {\n if (!NotificationKit.instance) {\n NotificationKit.instance = new NotificationKit()\n }\n return NotificationKit.instance\n }\n\n /**\n * Static init method for convenience\n */\n static async init(config: NotificationConfig): Promise<void> {\n return NotificationKit.getInstance().init(config)\n }\n\n /**\n * Initialize notification kit with configuration\n */\n async init(config: NotificationConfig): Promise<void> {\n if (this.initialized) {\n return\n }\n\n try {\n this.config = config\n await this.detectPlatform()\n await this.initializeProvider()\n await this.setupEventListeners()\n this.initialized = true\n this.emit('ready', {\n platform: this.platform,\n capabilities: this.capabilities,\n })\n } catch (error) {\n this.emit('error', { error, context: 'initialization' })\n throw error\n }\n }\n\n /**\n * Destroy notification kit instance\n */\n async destroy(): Promise<void> {\n if (this.provider) {\n await this.provider.destroy()\n this.provider = null\n }\n this.eventListeners.clear()\n this.initialized = false\n this.config = null\n NotificationKit.instance = null\n }\n\n /**\n * Check if notification kit is initialized\n */\n isInitialized(): boolean {\n return this.initialized\n }\n\n /**\n * Get current platform\n */\n getPlatform(): Platform {\n return this.platform\n }\n\n /**\n * Get platform capabilities\n */\n getCapabilities(): PlatformCapabilities | null {\n return this.capabilities\n }\n\n /**\n * Get current provider\n */\n getProvider(): NotificationProvider | null {\n return this.provider\n }\n\n /**\n * Request notification permission\n */\n async requestPermission(): Promise<boolean> {\n this.ensureInitialized()\n try {\n const granted = await this.provider!.requestPermission()\n this.emit('permissionChanged', {\n granted,\n status: granted ? 'granted' : 'denied',\n })\n return granted\n } catch (error) {\n this.emit('error', { error, context: 'permission' })\n throw error\n }\n }\n\n /**\n * Check notification permission status\n */\n async checkPermission(): Promise<PermissionStatus> {\n this.ensureInitialized()\n try {\n return await this.provider!.checkPermission()\n } catch (error) {\n this.emit('error', { error, context: 'permission' })\n throw error\n }\n }\n\n /**\n * Get notification token\n */\n async getToken(): Promise<string> {\n this.ensureInitialized()\n try {\n const token = await this.provider!.getToken()\n this.emit('tokenReceived', { token })\n return token\n } catch (error) {\n this.emit('error', { error, context: 'token' })\n throw error\n }\n }\n\n /**\n * Subscribe to topic\n */\n async subscribe(topic: string): Promise<void> {\n this.ensureInitialized()\n try {\n await this.provider!.subscribe(topic)\n this.emit('subscribed', { topic })\n } catch (error) {\n this.emit('error', { error, context: 'subscription' })\n throw error\n }\n }\n\n /**\n * Unsubscribe from topic\n */\n async unsubscribe(topic: string): Promise<void> {\n this.ensureInitialized()\n try {\n await this.provider!.unsubscribe(topic)\n this.emit('unsubscribed', { topic })\n } catch (error) {\n this.emit('error', { error, context: 'subscription' })\n throw error\n }\n }\n\n /**\n * Send push notification\n */\n async sendPushNotification(payload: PushNotificationPayload): Promise<void> {\n this.ensureInitialized()\n try {\n await this.provider!.sendNotification(payload)\n this.emit('notificationSent', { payload, type: 'push' })\n } catch (error) {\n this.emit('error', { error, context: 'push' })\n throw error\n }\n }\n\n /**\n * Schedule local notification\n */\n async scheduleLocalNotification(\n options: ScheduleOptions & LocalNotificationPayload\n ): Promise<void> {\n this.ensureInitialized()\n try {\n const localNotificationsModule = await DynamicLoader.loadLocalNotifications()\n if (!localNotificationsModule) {\n throw new Error('Local notifications are not available on this platform')\n }\n const { LocalNotifications } = localNotificationsModule\n const capacitorNotification = toCapacitorLocalNotification(options)\n await LocalNotifications.schedule({\n notifications: [capacitorNotification],\n })\n this.emit('notificationScheduled', { options, type: 'local' })\n } catch (error) {\n this.emit('error', { error, context: 'local' })\n throw error\n }\n }\n\n /**\n * Cancel local notification\n */\n async cancelLocalNotification(id: string | number): Promise<void> {\n this.ensureInitialized()\n try {\n const localNotificationsModule = await DynamicLoader.loadLocalNotifications()\n if (!localNotificationsModule) {\n throw new Error('Local notifications are not available on this platform')\n }\n const { LocalNotifications } = localNotificationsModule\n const numericId = typeof id === 'string' ? parseInt(id, 10) : id\n await LocalNotifications.cancel({\n notifications: [{ id: numericId }],\n })\n this.emit('notificationCancelled', { id, type: 'local' })\n } catch (error) {\n this.emit('error', { error, context: 'local' })\n throw error\n }\n }\n\n /**\n * Get pending local notifications\n */\n async getPendingLocalNotifications(): Promise<Notification[]> {\n this.ensureInitialized()\n try {\n const localNotificationsModule = await DynamicLoader.loadLocalNotifications()\n if (!localNotificationsModule) {\n throw new Error('Local notifications are not available on this platform')\n }\n const { LocalNotifications } = localNotificationsModule\n const result = await LocalNotifications.getPending()\n return result.notifications.map(n => ({\n id: n.id.toString(),\n title: n.title,\n body: n.body,\n data: n.extra,\n platform: this.platform,\n type: 'local',\n timestamp: new Date(),\n }))\n } catch (error) {\n this.emit('error', { error, context: 'local' })\n throw error\n }\n }\n\n /**\n * Show in-app notification\n */\n async showInAppNotification(options: InAppOptions): Promise<string> {\n try {\n // Import the in-app notification utility\n const { showInAppNotification } = await import('@/utils/inApp')\n const id = await showInAppNotification(options, this.config?.inApp)\n this.emit('notificationShown', { options, type: 'inApp', id })\n return id\n } catch (error) {\n this.emit('error', { error, context: 'inApp' })\n throw error\n }\n }\n\n /**\n * Check if notifications are supported\n */\n async isSupported(): Promise<boolean> {\n try {\n const { platform } = await import('@/core/platform')\n const capabilities = await platform.getCapabilities()\n return (\n capabilities.pushNotifications ||\n capabilities.localNotifications ||\n false\n )\n } catch (error) {\n // Support check failed, assume not supported\n return false\n }\n }\n\n /**\n * Create notification channel (Android)\n */\n async createChannel(channel: NotificationChannel): Promise<void> {\n if (this.platform !== 'android') {\n return\n }\n\n try {\n const localNotificationsModule = await DynamicLoader.loadLocalNotifications()\n if (!localNotificationsModule) {\n throw new Error('Local notifications are not available on this platform')\n }\n const { LocalNotifications } = localNotificationsModule\n const capacitorChannel = toCapacitorChannel(channel)\n await LocalNotifications.createChannel(capacitorChannel)\n this.emit('channelCreated', { channel })\n } catch (error) {\n this.emit('error', { error, context: 'channel' })\n throw error\n }\n }\n\n /**\n * Delete notification channel (Android)\n */\n async deleteChannel(channelId: string): Promise<void> {\n if (this.platform !== 'android') {\n return\n }\n\n try {\n const localNotificationsModule = await DynamicLoader.loadLocalNotifications()\n if (!localNotificationsModule) {\n throw new Error('Local notifications are not available on this platform')\n }\n const { LocalNotifications } = localNotificationsModule\n await LocalNotifications.deleteChannel({ id: channelId })\n this.emit('channelDeleted', { channelId })\n } catch (error) {\n this.emit('error', { error, context: 'channel' })\n throw error\n }\n }\n\n /**\n * List notification channels (Android)\n */\n async listChannels(): Promise<NotificationChannel[]> {\n if (this.platform !== 'android') {\n return []\n }\n\n try {\n const localNotificationsModule = await DynamicLoader.loadLocalNotifications()\n if (!localNotificationsModule) {\n throw new Error('Local notifications are not available on this platform')\n }\n const { LocalNotifications } = localNotificationsModule\n const result = await LocalNotifications.listChannels()\n return result.channels.map(channel => fromCapacitorChannel(channel))\n } catch (error) {\n this.emit('error', { error, context: 'channel' })\n throw error\n }\n }\n\n /**\n * Add event listener\n */\n on<T extends keyof NotificationEventMap>(\n event: T,\n callback: NotificationEventCallback<NotificationEventMap[T]>\n ): () => void {\n const listeners = this.eventListeners.get(event as string) || []\n listeners.push(callback as EventListener)\n this.eventListeners.set(event as string, listeners)\n\n // Return unsubscribe function\n return () => {\n const currentListeners = this.eventListeners.get(event as string) || []\n const index = currentListeners.indexOf(callback as EventListener)\n if (index > -1) {\n currentListeners.splice(index, 1)\n this.eventListeners.set(event as string, currentListeners)\n }\n }\n }\n\n /**\n * Remove event listener\n */\n off<T extends keyof NotificationEventMap>(\n event: T,\n callback?: NotificationEventCallback<NotificationEventMap[T]>\n ): void {\n if (!callback) {\n this.eventListeners.delete(event as string)\n return\n }\n\n const listeners = this.eventListeners.get(event as string) || []\n const index = listeners.indexOf(callback as EventListener)\n if (index > -1) {\n listeners.splice(index, 1)\n this.eventListeners.set(event as string, listeners)\n }\n }\n\n /**\n * Emit event to listeners\n */\n private emit(event: string, data: any): void {\n const listeners = this.eventListeners.get(event) || []\n const notificationEvent: NotificationEvent = {\n id: Date.now().toString(),\n type: event,\n timestamp: new Date(),\n data,\n ...data,\n }\n listeners.forEach(callback => {\n try {\n callback(notificationEvent)\n } catch (error) {\n // Event listener error, continue to next listener\n }\n })\n }\n\n /**\n * Detect current platform\n */\n private async detectPlatform(): Promise<void> {\n this.platform = await DynamicLoader.getPlatform()\n }\n\n /**\n * Initialize provider based on configuration\n */\n private async initializeProvider(): Promise<void> {\n if (!this.config) {\n throw new Error('Configuration is required')\n }\n\n try {\n if (this.config.provider === 'firebase') {\n const { FirebaseProvider } = await import(\n '@/providers/FirebaseProvider'\n )\n this.provider = new FirebaseProvider()\n } else if (this.config.provider === 'onesignal') {\n const { OneSignalProvider } = await import(\n '@/providers/OneSignalProvider'\n )\n this.provider = new OneSignalProvider()\n } else {\n throw new Error(`Unknown provider: ${this.config.provider}`)\n }\n\n await this.provider.init(this.config.config)\n const providerCapabilities = await this.provider.getCapabilities()\n this.capabilities = toPlatformCapabilities(providerCapabilities)\n } catch (error) {\n throw new Error(`Failed to initialize provider: ${error}`)\n }\n }\n\n /**\n * Setup event listeners for provider\n */\n private async setupEventListeners(): Promise<void> {\n if (!this.provider) {\n return\n }\n\n // Listen for provider messages\n this.provider.onMessage((payload: PushNotificationPayload) => {\n this.emit('notificationReceived', {\n payload,\n type: 'push',\n platform: this.platform,\n })\n })\n\n // Listen for token refresh\n this.provider.onTokenRefresh((token: string) => {\n this.emit('tokenRefreshed', { token })\n })\n\n // Listen for provider errors\n this.provider.onError((error: Error) => {\n this.emit('error', { error, context: 'provider' })\n })\n\n // Setup local notification listeners\n if (this.platform !== 'web') {\n try {\n const { LocalNotifications } = await import(\n '@capacitor/local-notifications'\n )\n\n LocalNotifications.addListener(\n 'localNotificationReceived',\n notification => {\n this.emit('notificationReceived', {\n payload: fromCapacitorLocalNotification(notification),\n type: 'local',\n platform: this.platform,\n })\n }\n )\n\n LocalNotifications.addListener(\n 'localNotificationActionPerformed',\n action => {\n this.emit('notificationActionPerformed', {\n action: action.actionId,\n notification: action.notification,\n inputValue: action.inputValue,\n platform: this.platform,\n })\n }\n )\n } catch (error) {\n // Local notifications not available on this platform\n }\n }\n }\n\n /**\n * Ensure notification kit is initialized\n */\n private ensureInitialized(): void {\n if (!this.initialized) {\n throw new Error('NotificationKit must be initialized before use')\n }\n }\n}\n\n/**\n * Convenience methods for common operations\n */\nexport const notifications = {\n /**\n * Initialize notification kit\n */\n init: (config: NotificationConfig) =>\n NotificationKit.getInstance().init(config),\n\n /**\n * Request permission\n */\n requestPermission: () => NotificationKit.getInstance().requestPermission(),\n\n /**\n * Check permission\n */\n checkPermission: () => NotificationKit.getInstance().checkPermission(),\n\n /**\n * Check if permission is granted\n */\n isPermissionGranted: async () => {\n const kit = NotificationKit.getInstance()\n const status = await kit.checkPermission()\n return status === 'granted'\n },\n\n /**\n * Get permission state\n */\n getPermissionState: () => NotificationKit.getInstance().checkPermission(),\n\n /**\n * Get token\n */\n getToken: () => NotificationKit.getInstance().getToken(),\n\n /**\n * Delete token\n */\n deleteToken: async () => {\n const kit = NotificationKit.getInstance()\n const provider = kit.getProvider()\n if (provider && 'deleteToken' in provider) {\n await provider.deleteToken()\n } else {\n throw new Error('deleteToken not supported by current provider')\n }\n },\n\n /**\n * Subscribe to topic\n */\n subscribe: (topic: string) => NotificationKit.getInstance().subscribe(topic),\n\n /**\n * Unsubscribe from topic\n */\n unsubscribe: (topic: string) =>\n NotificationKit.getInstance().unsubscribe(topic),\n\n /**\n * Schedule local notification\n */\n schedule: (options: ScheduleOptions & LocalNotificationPayload) =>\n NotificationKit.getInstance().scheduleLocalNotification(options),\n\n /**\n * Cancel local notification\n */\n cancel: (id: number) =>\n NotificationKit.getInstance().cancelLocalNotification(id),\n\n /**\n * Get pending notifications\n */\n getPending: () =>\n NotificationKit.getInstance().getPendingLocalNotifications(),\n\n /**\n * Get delivered notifications\n */\n getDelivered: async () => {\n const kit = NotificationKit.getInstance()\n if (kit.getPlatform() === 'web') {\n throw new Error('getDelivered not supported on web platform')\n }\n const localNotificationsModule = await DynamicLoader.loadLocalNotifications()\n if (!localNotificationsModule) {\n throw new Error('Local notifications are not available on this platform')\n }\n const { LocalNotifications } = localNotificationsModule\n const result = await LocalNotifications.getDeliveredNotifications()\n return result.notifications\n },\n\n /**\n * Remove delivered notification\n */\n removeDelivered: async (id: string) => {\n const kit = NotificationKit.getInstance()\n if (kit.getPlatform() === 'web') {\n throw new Error('removeDelivered not supported on web platform')\n }\n const localNotificationsModule = await DynamicLoader.loadLocalNotifications()\n if (!localNotificationsModule) {\n throw new Error('Local notifications are not available on this platform')\n }\n const { LocalNotifications } = localNotificationsModule\n await LocalNotifications.removeDeliveredNotifications({\n notifications: [{ id: parseInt(id, 10), title: '', body: '' }]\n })\n },\n\n /**\n * Remove all delivered notifications\n */\n removeAllDelivered: async () => {\n const kit = NotificationKit.getInstance()\n if (kit.getPlatform() === 'web') {\n throw new Error('removeAllDelivered not supported on web platform')\n }\n const localNotificationsModule = await DynamicLoader.loadLocalNotifications()\n if (!localNotificationsModule) {\n throw new Error('Local notifications are not available on this platform')\n }\n const { LocalNotifications } = localNotificationsModule\n await LocalNotifications.removeAllDeliveredNotifications()\n },\n\n /**\n * Cancel all pending notifications\n */\n cancelAll: async () => {\n const kit = NotificationKit.getInstance()\n if (kit.getPlatform() === 'web') {\n throw new Error('cancelAll not supported on web platform')\n }\n const localNotificationsModule = await DynamicLoader.loadLocalNotifications()\n if (!localNotificationsModule) {\n throw new Error('Local notifications are not available on this platform')\n }\n const { LocalNotifications } = localNotificationsModule\n const pending = await LocalNotifications.getPending()\n if (pending.notifications.length > 0) {\n await LocalNotifications.cancel({\n notifications: pending.notifications.map(n => ({ id: n.id }))\n })\n }\n },\n\n /**\n * Listen for push notifications\n */\n onPush: (callback: (notification: any) => void) => {\n return NotificationKit.getInstance().on('notificationReceived', (event) => {\n if (event.type.startsWith('push.')) {\n callback(event.notification)\n }\n })\n },\n\n /**\n * Listen for push notification opened\n */\n onPushOpened: (callback: (notification: any) => void) => {\n return NotificationKit.getInstance().on('notificationActionPerformed', (event) => {\n if (event.type === 'push.opened' || (event.type === 'push.action' && event.actionId === 'tap')) {\n callback(event.notification)\n }\n })\n },\n\n /**\n * Show in-app notification\n */\n showInApp: (options: InAppOptions): Promise<string> =>\n NotificationKit.getInstance().showInAppNotification(options),\n\n /**\n * Success notification\n */\n success: (title: string, message?: string): Promise<string> =>\n NotificationKit.getInstance().showInAppNotification({\n title,\n message: message ?? title,\n type: 'success',\n }),\n\n /**\n * Error notification\n */\n error: (title: string, message?: string): Promise<string> =>\n NotificationKit.getInstance().showInAppNotification({\n title,\n message: message ?? title,\n type: 'error',\n }),\n\n /**\n * Warning notification\n */\n warning: (title: string, message?: string): Promise<string> =>\n NotificationKit.getInstance().showInAppNotification({\n title,\n message: message ?? title,\n type: 'warning',\n }),\n\n /**\n * Info notification\n */\n info: (title: string, message?: string): Promise<string> =>\n NotificationKit.getInstance().showInAppNotification({\n title,\n message: message ?? title,\n type: 'info',\n }),\n\n /**\n * Add event listener\n */\n on: <T extends keyof NotificationEventMap>(\n event: T,\n callback: NotificationEventCallback<NotificationEventMap[T]>\n ) => NotificationKit.getInstance().on(event, callback),\n\n /**\n * Remove event listener\n */\n off: <T extends keyof NotificationEventMap>(\n event: T,\n callback?: NotificationEventCallback<NotificationEventMap[T]>\n ) => NotificationKit.getInstance().off(event, callback),\n}\n","import type { PermissionStatus, Platform } from '@/types'\nimport { DynamicLoader } from '@/utils/dynamic-loader'\n\n/**\n * Permission management utilities\n */\nexport class PermissionManager {\n private platform: Platform\n\n constructor() {\n this.platform = 'unknown' // Will be detected on first use\n }\n\n /**\n * Request notification permissions\n */\n async requestPermission(): Promise<boolean> {\n try {\n await this.ensurePlatform()\n if (this.platform === 'web') {\n return await this.requestWebPermission()\n } else {\n return await this.requestNativePermission()\n }\n } catch (error) {\n // Permission request failed, return false\n return false\n }\n }\n\n /**\n * Check current permission status\n */\n async checkPermission(): Promise<PermissionStatus> {\n try {\n await this.ensurePlatform()\n if (this.platform === 'web') {\n return await this.checkWebPermission()\n } else {\n return await this.checkNativePermission()\n }\n } catch (error) {\n // Permission check failed, assume denied\n return 'denied'\n }\n }\n\n /**\n * Check if permission is granted\n */\n async isPermissionGranted(): Promise<boolean> {\n const status = await this.checkPermission()\n return status === 'granted'\n }\n\n /**\n * Check if permission can be requested\n */\n async canRequestPermission(): Promise<boolean> {\n const status = await this.checkPermission()\n return status === 'prompt' || status === 'default'\n }\n\n /**\n * Open system settings for notifications\n */\n async openSettings(): Promise<void> {\n if (this.platform === 'web') {\n // Can't open settings from web\n throw new Error('Cannot open settings from web platform')\n }\n\n try {\n // TODO: Add native settings support when package is available\n // const { NativeSettings } = await import('@capacitor-community/native-settings')\n // await NativeSettings.open({\n // optionAndroid: 'APPLICATION_DETAILS_SETTINGS',\n // optionIOS: 'App-Prefs:NOTIFICATIONS_ID'\n // })\n // Native settings functionality not yet implemented\n } catch (error) {\n // Failed to open settings\n throw error\n }\n }\n\n /**\n * Request web notification permission\n */\n private async requestWebPermission(): Promise<boolean> {\n if (!('Notification' in window)) {\n throw new Error('Notifications not supported in this browser')\n }\n\n if (Notification.permission === 'granted') {\n return true\n }\n\n if (Notification.permission === 'denied') {\n return false\n }\n\n const permission = await Notification.requestPermission()\n return permission === 'granted'\n }\n\n /**\n * Check web notification permission\n */\n private async checkWebPermission(): Promise<PermissionStatus> {\n if (!('Notification' in window)) {\n return 'denied'\n }\n\n const permission = Notification.permission\n\n // Map web permission values to our PermissionStatus type\n if (permission === 'default') {\n return 'prompt'\n }\n return permission as PermissionStatus\n }\n\n /**\n * Request native notification permission\n */\n private async requestNativePermission(): Promise<boolean> {\n try {\n const pushNotificationsModule = await DynamicLoader.loadPushNotifications()\n if (!pushNotificationsModule) {\n throw new Error('Push notifications are not available on this platform')\n }\n const { PushNotifications } = pushNotificationsModule\n const result = await PushNotifications.requestPermissions()\n return result.receive === 'granted'\n } catch (error) {\n // Native permission request failed\n return false\n }\n }\n\n /**\n * Check native notification permission\n */\n private async checkNativePermission(): Promise<PermissionStatus> {\n try {\n const pushNotificationsModule = await DynamicLoader.loadPushNotifications()\n if (!pushNotificationsModule) {\n throw new Error('Push notifications are not available on this platform')\n }\n const { PushNotifications } = pushNotificationsModule\n const result = await PushNotifications.checkPermissions()\n\n // Map Capacitor permission values to our PermissionStatus type\n if (result.receive === 'granted') {\n return 'granted'\n } else if (result.receive === 'denied') {\n return 'denied'\n } else if (result.receive === 'prompt') {\n return 'prompt'\n } else {\n return 'unknown'\n }\n } catch (error) {\n // Native permission check failed\n return 'denied'\n }\n }\n\n /**\n * Detect current platform\n */\n private async detectPlatform(): Promise<Platform> {\n return DynamicLoader.getPlatform()\n }\n \n /**\n * Ensure platform is detected\n */\n private async ensurePlatform(): Promise<void> {\n if (this.platform === 'unknown') {\n this.platform = await this.detectPlatform()\n }\n }\n}\n\n/**\n * Global permission manager instance\n */\nexport const permissionManager = new PermissionManager()\n\n/**\n * Convenience functions\n */\nexport const permissions = {\n request: () => permissionManager.requestPermission(),\n check: () => permissionManager.checkPermission(),\n isGranted: () => permissionManager.isPermissionGranted(),\n canRequest: () => permissionManager.canRequestPermission(),\n openSettings: () => permissionManager.openSettings(),\n}\n","import type {\n Platform,\n PlatformCapabilities,\n PlatformDetection,\n PlatformDefaults,\n PlatformCompatibility,\n} from '@/types'\nimport { DynamicLoader } from '@/utils/dynamic-loader'\n\n/**\n * Platform detection and capability management\n */\nexport class PlatformManager {\n private detection: PlatformDetection | null = null\n private capabilities: PlatformCapabilities | null = null\n\n /**\n * Detect current platform\n */\n async detect(): Promise<PlatformDetection> {\n if (this.detection) {\n return this.detection\n }\n\n const platform = await this.getPlatform()\n const isCapacitor = await DynamicLoader.isNativePlatform()\n const isHybrid = isCapacitor\n const isNative = isCapacitor\n const isWeb = platform === 'web'\n const isMobile = platform === 'ios' || platform === 'android'\n const isDesktop = platform === 'electron' || (!isMobile && isWeb)\n const isTablet = false // TODO: Implement tablet detection\n const userAgent =\n typeof navigator !== 'undefined' ? navigator.userAgent : ''\n const version = this.getVersion()\n\n this.detection = {\n platform,\n isCapacitor,\n isHybrid,\n isNative,\n isWeb,\n isMobile,\n isDesktop,\n isTablet,\n version,\n userAgent,\n supportedFeatures: this.getSupportedFeatures(platform),\n limitations: this.getLimitations(platform),\n warnings: this.getWarnings(platform),\n }\n\n return this.detection\n }\n\n /**\n * Get platform capabilities\n */\n async getCapabilities(platform?: Platform): Promise<PlatformCapabilities> {\n const targetPlatform = platform || await this.getPlatform()\n\n if (this.capabilities && !platform) {\n return this.capabilities\n }\n\n const capabilities = this.buildCapabilities(targetPlatform)\n\n if (!platform) {\n this.capabilities = capabilities\n }\n\n return capabilities\n }\n\n /**\n * Check if feature is supported\n */\n async isSupported(\n feature: keyof PlatformCapabilities,\n platform?: Platform\n ): Promise<boolean> {\n const capabilities = await this.getCapabilities(platform)\n return capabilities[feature] || false\n }\n\n /**\n * Get platform defaults\n */\n getDefaults(): PlatformDefaults {\n return {\n web: {\n pushNotifications: true,\n localNotifications: false,\n inAppNotifications: true,\n notificationChannels: false,\n notificationActions: true,\n notificationBadging: false,\n notificationSound: true,\n notificationVibration: false,\n serviceWorker: true,\n webPushProtocol: true,\n backgroundSync: true,\n },\n ios: {\n pushNotifications: true,\n localNotifications: true,\n inAppNotifications: true,\n notificationChannels: false,\n notificationActions: true,\n notificationBadging: true,\n notificationSound: true,\n notificationVibration: true,\n criticalAlerts: true,\n provisionalAuth: true,\n appBadge: true,\n },\n android: {\n pushNotifications: true,\n localNotifications: true,\n inAppNotifications: true,\n notificationChannels: true,\n notificationActions: true,\n notificationBadging: true,\n notificationSound: true,\n notificationVibration: true,\n notificationLights: true,\n notificationGrouping: true,\n notificationImportance: true,\n notificationVisibility: true,\n foregroundService: true,\n },\n electron: {\n pushNotifications: true,\n localNotifications: true,\n inAppNotifications: true,\n notificationActions: true,\n notificationBadging: true,\n notificationSound: true,\n },\n unknown: {\n pushNotifications: false,\n localNotifications: false,\n inAppNotifications: false,\n notificationChannels: false,\n notificationActions: false,\n notificationBadging: false,\n notificationSound: false,\n notificationVibration: false,\n },\n }\n }\n\n /**\n * Get platform compatibility matrix\n */\n getCompatibility(): PlatformCompatibility {\n return {\n pushNotifications: {\n web: true,\n ios: true,\n android: true,\n electron: true,\n notes: 'Requires service worker on web',\n },\n localNotifications: {\n web: false,\n ios: true,\n android: true,\n electron: true,\n notes: 'Not supported in browsers',\n },\n inAppNotifications: {\n web: true,\n ios: true,\n android: true,\n electron: true,\n notes: 'Custom implementation across platforms',\n },\n notificationChannels: {\n web: false,\n ios: false,\n android: