forked from trent_larson/crowd-funder-for-time-pwa
feat(notifications): integrate daily notification plugin into AccountViewView
- Add notification methods to PlatformService interface - Implement notification support in CapacitorPlatformService with plugin integration - Add stub implementations in WebPlatformService and ElectronPlatformService - Add nativeNotificationTime, nativeNotificationTitle, and nativeNotificationMessage fields to Settings interface - Create DailyNotificationSection component for AccountViewView integration - Add Android manifest permissions (POST_NOTIFICATIONS, SCHEDULE_EXACT_ALARM, RECEIVE_BOOT_COMPLETED) - Register daily-notification-plugin in capacitor.plugins.json - Integrate DailyNotificationSection into AccountViewView Features: - Platform capability detection (hides on unsupported platforms) - Permission request flow with fallback to settings - Schedule/cancel notifications - Time editing with HTML5 time input - Settings persistence - Plugin state synchronization on app load NOTE: Currently storing notification schedule in SQLite database, but plugin was designed to store schedule internally. Consider migrating to plugin's internal storage to avoid database initialization issues.
This commit is contained in:
@@ -20,6 +20,11 @@ import {
|
||||
ImageResult,
|
||||
PlatformService,
|
||||
PlatformCapabilities,
|
||||
NotificationStatus,
|
||||
PermissionStatus,
|
||||
PermissionResult,
|
||||
ScheduleOptions,
|
||||
NativeFetcherConfig,
|
||||
} from "../PlatformService";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { BaseDatabaseService } from "./BaseDatabaseService";
|
||||
@@ -1333,6 +1338,359 @@ export class CapacitorPlatformService
|
||||
// --- PWA/Web-only methods (no-op for Capacitor) ---
|
||||
public registerServiceWorker(): void {}
|
||||
|
||||
// Daily notification operations
|
||||
/**
|
||||
* Get the status of scheduled daily notifications
|
||||
* @see PlatformService.getDailyNotificationStatus
|
||||
*/
|
||||
async getDailyNotificationStatus(): Promise<NotificationStatus | null> {
|
||||
try {
|
||||
// Dynamic import to avoid build issues if plugin unavailable
|
||||
const { DailyNotification } = await import(
|
||||
"@timesafari/daily-notification-plugin"
|
||||
);
|
||||
|
||||
const pluginStatus = await DailyNotification.getNotificationStatus();
|
||||
|
||||
// Get permissions separately
|
||||
const permissions = await DailyNotification.checkPermissions();
|
||||
|
||||
// Map plugin PermissionState to our PermissionStatus format
|
||||
const notificationsPermission = permissions.notifications;
|
||||
let notifications: "granted" | "denied" | "prompt";
|
||||
|
||||
if (notificationsPermission === "granted") {
|
||||
notifications = "granted";
|
||||
} else if (notificationsPermission === "denied") {
|
||||
notifications = "denied";
|
||||
} else {
|
||||
notifications = "prompt";
|
||||
}
|
||||
|
||||
// Handle lastNotificationTime which can be a Promise<number>
|
||||
let lastTriggered: string | undefined;
|
||||
const lastNotificationTime = pluginStatus.lastNotificationTime;
|
||||
if (lastNotificationTime) {
|
||||
const timeValue = await Promise.resolve(lastNotificationTime);
|
||||
if (typeof timeValue === "number") {
|
||||
lastTriggered = new Date(timeValue).toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isScheduled: pluginStatus.isScheduled ?? false,
|
||||
scheduledTime: pluginStatus.settings?.time,
|
||||
lastTriggered,
|
||||
permissions: {
|
||||
notifications,
|
||||
exactAlarms: undefined, // Plugin doesn't expose this in status
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[CapacitorPlatformService] Failed to get notification status:",
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check notification permissions
|
||||
* @see PlatformService.checkNotificationPermissions
|
||||
*/
|
||||
async checkNotificationPermissions(): Promise<PermissionStatus | null> {
|
||||
try {
|
||||
const { DailyNotification } = await import(
|
||||
"@timesafari/daily-notification-plugin"
|
||||
);
|
||||
|
||||
const permissions = await DailyNotification.checkPermissions();
|
||||
|
||||
// Log the raw permission state for debugging
|
||||
logger.info(
|
||||
`[CapacitorPlatformService] Raw permission state from plugin:`,
|
||||
permissions,
|
||||
);
|
||||
|
||||
// Map plugin PermissionState to our PermissionStatus format
|
||||
const notificationsPermission = permissions.notifications;
|
||||
let notifications: "granted" | "denied" | "prompt";
|
||||
|
||||
// Handle all possible PermissionState values
|
||||
if (notificationsPermission === "granted") {
|
||||
notifications = "granted";
|
||||
} else if (
|
||||
notificationsPermission === "denied" ||
|
||||
notificationsPermission === "ephemeral"
|
||||
) {
|
||||
notifications = "denied";
|
||||
} else {
|
||||
// Treat "prompt", "prompt-with-rationale", "unknown", "provisional" as "prompt"
|
||||
// This allows Android to show the permission dialog
|
||||
notifications = "prompt";
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[CapacitorPlatformService] Mapped permission state: ${notifications} (from ${notificationsPermission})`,
|
||||
);
|
||||
|
||||
return {
|
||||
notifications,
|
||||
exactAlarms: undefined, // Plugin doesn't expose this directly
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[CapacitorPlatformService] Failed to check permissions:",
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request notification permissions
|
||||
* @see PlatformService.requestNotificationPermissions
|
||||
*/
|
||||
async requestNotificationPermissions(): Promise<PermissionResult | null> {
|
||||
try {
|
||||
const { DailyNotification } = await import(
|
||||
"@timesafari/daily-notification-plugin"
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`[CapacitorPlatformService] Requesting notification permissions...`,
|
||||
);
|
||||
|
||||
const result = await DailyNotification.requestPermissions();
|
||||
|
||||
logger.info(
|
||||
`[CapacitorPlatformService] Permission request result:`,
|
||||
result,
|
||||
);
|
||||
|
||||
// Map plugin PermissionState to boolean
|
||||
const notificationsGranted = result.notifications === "granted";
|
||||
|
||||
logger.info(
|
||||
`[CapacitorPlatformService] Mapped permission result: ${notificationsGranted} (from ${result.notifications})`,
|
||||
);
|
||||
|
||||
return {
|
||||
notifications: notificationsGranted,
|
||||
exactAlarms: undefined, // Plugin doesn't expose this directly
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[CapacitorPlatformService] Failed to request permissions:",
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a daily notification
|
||||
* @see PlatformService.scheduleDailyNotification
|
||||
*/
|
||||
async scheduleDailyNotification(options: ScheduleOptions): Promise<void> {
|
||||
try {
|
||||
const { DailyNotification } = await import(
|
||||
"@timesafari/daily-notification-plugin"
|
||||
);
|
||||
|
||||
await DailyNotification.scheduleDailyNotification({
|
||||
time: options.time,
|
||||
title: options.title,
|
||||
body: options.body,
|
||||
sound: options.sound ?? true,
|
||||
priority: options.priority ?? "high",
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`[CapacitorPlatformService] Scheduled daily notification for ${options.time}`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[CapacitorPlatformService] Failed to schedule notification:",
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel scheduled daily notification
|
||||
* @see PlatformService.cancelDailyNotification
|
||||
*/
|
||||
async cancelDailyNotification(): Promise<void> {
|
||||
try {
|
||||
const { DailyNotification } = await import(
|
||||
"@timesafari/daily-notification-plugin"
|
||||
);
|
||||
|
||||
await DailyNotification.cancelAllNotifications();
|
||||
|
||||
logger.info("[CapacitorPlatformService] Cancelled daily notification");
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[CapacitorPlatformService] Failed to cancel notification:",
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure native fetcher for background operations
|
||||
* @see PlatformService.configureNativeFetcher
|
||||
*/
|
||||
async configureNativeFetcher(
|
||||
config: NativeFetcherConfig,
|
||||
): Promise<void | null> {
|
||||
try {
|
||||
const { DailyNotification } = await import(
|
||||
"@timesafari/daily-notification-plugin"
|
||||
);
|
||||
|
||||
// Plugin expects apiBaseUrl, activeDid, and jwtToken
|
||||
// We'll need to get activeDid from somewhere - for now pass empty string
|
||||
// Components should provide activeDid when calling this
|
||||
await DailyNotification.configureNativeFetcher({
|
||||
apiBaseUrl: config.apiServer,
|
||||
activeDid: "", // Should be provided by caller
|
||||
jwtToken: config.jwt,
|
||||
});
|
||||
|
||||
logger.info("[CapacitorPlatformService] Configured native fetcher");
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[CapacitorPlatformService] Failed to configure native fetcher:",
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update starred plans for background fetcher
|
||||
* @see PlatformService.updateStarredPlans
|
||||
*/
|
||||
async updateStarredPlans(plans: { planIds: string[] }): Promise<void | null> {
|
||||
try {
|
||||
const { DailyNotification } = await import(
|
||||
"@timesafari/daily-notification-plugin"
|
||||
);
|
||||
|
||||
await DailyNotification.updateStarredPlans({
|
||||
planIds: plans.planIds,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`[CapacitorPlatformService] Updated starred plans: ${plans.planIds.length} plans`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[CapacitorPlatformService] Failed to update starred plans:",
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the app's notification settings in the system settings
|
||||
* @see PlatformService.openAppNotificationSettings
|
||||
*/
|
||||
async openAppNotificationSettings(): Promise<void | null> {
|
||||
try {
|
||||
const platform = Capacitor.getPlatform();
|
||||
|
||||
if (platform === "android") {
|
||||
// Android: Open app details settings page
|
||||
// From there, users can navigate to "Notifications" section
|
||||
// This is more reliable than trying to open notification settings directly
|
||||
const packageName = "app.timesafari.app"; // Full application ID from build.gradle
|
||||
|
||||
// Use APPLICATION_DETAILS_SETTINGS which opens the app's settings page
|
||||
// Users can then navigate to "Notifications" section
|
||||
// Try multiple URL formats to ensure compatibility
|
||||
const intentUrl1 = `intent:#Intent;action=android.settings.APPLICATION_DETAILS_SETTINGS;data=package:${packageName};end`;
|
||||
const intentUrl2 = `intent://settings/app_detail?package=${packageName}#Intent;scheme=android-app;end`;
|
||||
|
||||
logger.info(
|
||||
`[CapacitorPlatformService] Opening Android app settings for ${packageName}`,
|
||||
);
|
||||
|
||||
// Log current permission state before opening settings
|
||||
try {
|
||||
const currentPerms = await this.checkNotificationPermissions();
|
||||
logger.info(
|
||||
`[CapacitorPlatformService] Current permission state before opening settings:`,
|
||||
currentPerms,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`[CapacitorPlatformService] Could not check permissions before opening settings:`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
|
||||
// Try multiple approaches to ensure it works
|
||||
try {
|
||||
// Method 1: Direct window.location.href (most reliable)
|
||||
window.location.href = intentUrl1;
|
||||
|
||||
// Method 2: Fallback with window.open
|
||||
setTimeout(() => {
|
||||
try {
|
||||
window.open(intentUrl1, "_blank");
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
"[CapacitorPlatformService] window.open fallback failed:",
|
||||
e,
|
||||
);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Method 3: Alternative format
|
||||
setTimeout(() => {
|
||||
try {
|
||||
window.location.href = intentUrl2;
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
"[CapacitorPlatformService] Alternative format failed:",
|
||||
e,
|
||||
);
|
||||
}
|
||||
}, 200);
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"[CapacitorPlatformService] Failed to open intent URL:",
|
||||
e,
|
||||
);
|
||||
}
|
||||
} else if (platform === "ios") {
|
||||
// iOS: Use app settings URL scheme
|
||||
const settingsUrl = `app-settings:`;
|
||||
window.location.href = settingsUrl;
|
||||
|
||||
logger.info("[CapacitorPlatformService] Opening iOS app settings");
|
||||
} else {
|
||||
logger.warn(
|
||||
`[CapacitorPlatformService] Cannot open settings on platform: ${platform}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[CapacitorPlatformService] Failed to open app notification settings:",
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Database utility methods - inherited from BaseDatabaseService
|
||||
// generateInsertStatement, updateDefaultSettings, updateActiveDid,
|
||||
// getActiveIdentity, insertNewDidIntoSettings, updateDidSpecificSettings,
|
||||
|
||||
Reference in New Issue
Block a user