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:
Jose Olarte III
2026-02-13 18:44:38 +08:00
parent c05dff6654
commit c2fb493073
2 changed files with 143 additions and 50 deletions

View File

@@ -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;
}