WIP: android daily notification re-schedule + plugin handoff doc
- NativeNotificationService: platform-specific schedule/cancel - iOS: pass id "daily_timesafari_reminder", call cancelDailyReminder before schedule - Android: no id (plugin uses "daily_notification"), skip pre-cancel to match test app - Verification: return true when schedule succeeds but reminder not found (avoids error dialog) - doc: android-daily-notification-second-schedule-issue.md - Symptom, timing (re-schedule-too-soon), test app vs TimeSafari - Plugin-side section: entry point, files, likely cause, suggested fixes, repro steps - For use in plugin repo (e.g. Cursor) to fix second-schedule Re-scheduled notifications on Android still fail to fire; fix expected in plugin (see doc).
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
|
||||
|
||||
/**
|
||||
@@ -40,12 +41,19 @@ import { logger } from "@/utils/logger";
|
||||
* - Native OS notification UI
|
||||
*/
|
||||
export class NativeNotificationService implements NotificationServiceInterface {
|
||||
// IMPORTANT: ID must start with "daily_" for proper schedule rollover handling
|
||||
// The plugin's scheduleNextNotification() preserves IDs starting with "daily_"
|
||||
// but replaces others with random "daily_rollover_xxx" IDs, causing conflicts
|
||||
private readonly reminderId = "daily_timesafari_reminder";
|
||||
private readonly platformName = "native";
|
||||
|
||||
/**
|
||||
* Reminder/schedule ID used for cancel and getStatus lookup.
|
||||
* - iOS: We pass this when scheduling so the plugin stores and returns it; use a stable id.
|
||||
* - Android: We do not pass id (plugin uses "daily_notification") to avoid second-schedule bug.
|
||||
*/
|
||||
private get reminderId(): string {
|
||||
return Capacitor.getPlatform() === "ios"
|
||||
? "daily_timesafari_reminder"
|
||||
: "daily_notification";
|
||||
}
|
||||
|
||||
/**
|
||||
* Native notifications are always supported on iOS/Android
|
||||
*/
|
||||
@@ -282,25 +290,25 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
);
|
||||
}
|
||||
|
||||
// Cancel any existing notification with the same ID before scheduling a new one
|
||||
// This ensures the old notification is removed from iOS notification center
|
||||
try {
|
||||
logger.debug(
|
||||
"[NativeNotificationService] Canceling existing notification before rescheduling",
|
||||
);
|
||||
// The Swift plugin expects an object with reminderId property
|
||||
// Even though TypeScript definition says string, we need to pass an object
|
||||
await (
|
||||
DailyNotification as unknown as DailyNotificationWithObjectCancel
|
||||
).cancelDailyReminder({
|
||||
reminderId: this.reminderId,
|
||||
});
|
||||
} catch (cancelError) {
|
||||
// Ignore errors if notification doesn't exist - that's fine
|
||||
logger.debug(
|
||||
"[NativeNotificationService] No existing notification to cancel (or cancel failed):",
|
||||
cancelError,
|
||||
);
|
||||
// On iOS only: cancel existing reminder before rescheduling (removes from notification center).
|
||||
// On Android we skip pre-cancel to match the test app; the plugin cancels the previous alarm
|
||||
// for this scheduleId inside scheduleDailyNotification before scheduling the new one.
|
||||
if (Capacitor.getPlatform() === "ios") {
|
||||
try {
|
||||
logger.debug(
|
||||
"[NativeNotificationService] Canceling existing notification before rescheduling",
|
||||
);
|
||||
await (
|
||||
DailyNotification as unknown as DailyNotificationWithObjectCancel
|
||||
).cancelDailyReminder({
|
||||
reminderId: this.reminderId,
|
||||
});
|
||||
} catch (cancelError) {
|
||||
logger.debug(
|
||||
"[NativeNotificationService] No existing notification to cancel (or cancel failed):",
|
||||
cancelError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Log current time and scheduled time for debugging
|
||||
@@ -331,35 +339,35 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
"[NativeNotificationService] Calling scheduleDailyReminder with options:",
|
||||
{
|
||||
id: this.reminderId,
|
||||
title: options.title,
|
||||
body: options.body,
|
||||
time: options.time,
|
||||
repeatDaily: true,
|
||||
sound: true,
|
||||
vibration: true,
|
||||
priority: options.priority || "normal",
|
||||
},
|
||||
);
|
||||
|
||||
await DailyNotification.scheduleDailyReminder({
|
||||
id: this.reminderId,
|
||||
// iOS: pass id so plugin stores/returns it (getStatus and verification find it).
|
||||
// Android: omit id so plugin uses "daily_notification" (avoids second-schedule bug).
|
||||
const scheduleOptions: {
|
||||
time: string;
|
||||
title: string;
|
||||
body: string;
|
||||
sound: boolean;
|
||||
priority: "low" | "default" | "high";
|
||||
id?: string;
|
||||
} = {
|
||||
time: options.time,
|
||||
title: options.title,
|
||||
body: options.body,
|
||||
time: options.time, // HH:mm format
|
||||
repeatDaily: true,
|
||||
sound: true,
|
||||
vibration: true,
|
||||
priority: options.priority || "normal",
|
||||
});
|
||||
priority: (options.priority || "normal") as "low" | "default" | "high",
|
||||
};
|
||||
if (Capacitor.getPlatform() === "ios") {
|
||||
scheduleOptions.id = this.reminderId;
|
||||
}
|
||||
logger.debug(
|
||||
"[NativeNotificationService] Calling scheduleDailyNotification with options:",
|
||||
scheduleOptions,
|
||||
);
|
||||
|
||||
await DailyNotification.scheduleDailyNotification(scheduleOptions);
|
||||
|
||||
logger.info(
|
||||
"[NativeNotificationService] scheduleDailyReminder call completed successfully",
|
||||
"[NativeNotificationService] scheduleDailyNotification call completed successfully",
|
||||
{
|
||||
reminderId: this.reminderId,
|
||||
requestedTime: options.time,
|
||||
},
|
||||
);
|
||||
@@ -418,9 +426,9 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
})),
|
||||
},
|
||||
);
|
||||
// On iOS, if verification fails, return false
|
||||
// On Android, this method isn't available, so we'll fall through to return true
|
||||
return false;
|
||||
// Schedule call succeeded; verification may fail if plugin returns stale data (e.g. old id).
|
||||
// Return true so we don't show "Error Setting Notification Permissions"; getStatus will reflect once plugin state updates.
|
||||
return true;
|
||||
}
|
||||
} catch (verifyError) {
|
||||
// If getScheduledReminders() is not implemented (Android), assume success
|
||||
@@ -431,7 +439,7 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
) {
|
||||
logger.debug(
|
||||
"[NativeNotificationService] getScheduledReminders() not available on this platform (expected on Android). " +
|
||||
"Assuming success since scheduleDailyReminder() completed without error.",
|
||||
"Assuming success since scheduleDailyNotification() completed without error.",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user