Fix: Android daily notification: single schedule on edit, no double-cancel
Resolves long-standing issue where the second scheduled time (after editing
the reminder) did not fire on Android.
- PushNotificationPermission: add open(..., options?: { skipSchedule }).
When skipSchedule is true (edit flow), dialog only invokes callback with
time/message; parent is sole scheduler so the plugin is not called twice.
- AccountViewView: pass { skipSchedule: true } when opening the dialog for
edit; keep cancel (iOS only) + single scheduleDailyNotification in callback.
- NativeNotificationService: serialize scheduleDailyNotification so only one
schedule runs at a time (scheduleLock + doScheduleDailyNotification).
- AccountViewView: guard edit-reminder callback with editReminderScheduleInProgress
so one schedule per user action.
- Gate pre-cancel on Android in edit flow (CONSUMING_APP brief): skip
cancelDailyNotification before schedule on Android; plugin cancels internally.
- Use single stable reminder id and always pass id on both platforms (plugin 1.1.2+).
- Add doc/plugin-android-edit-reschedule-alarm-not-firing.md for plugin repo
(cancel-before-reschedule may cancel the PendingIntent used for setAlarmClock).
This commit is contained in:
@@ -156,6 +156,8 @@ export default class PushNotificationPermission extends Vue {
|
||||
messageInput = "";
|
||||
minuteInput = "00";
|
||||
pushType = "";
|
||||
/** When true, dialog only returns time/message to parent; parent does cancel+schedule (avoids double schedule on edit). */
|
||||
skipScheduleForOpen = false;
|
||||
serviceWorkerReady = false;
|
||||
vapidKey = "";
|
||||
|
||||
@@ -169,10 +171,12 @@ export default class PushNotificationPermission extends Vue {
|
||||
async open(
|
||||
pushType: string,
|
||||
callback?: (success: boolean, time: string, message?: string) => void,
|
||||
options?: { skipSchedule?: boolean },
|
||||
) {
|
||||
this.callback = callback || this.callback;
|
||||
this.isVisible = true;
|
||||
this.pushType = pushType;
|
||||
this.skipScheduleForOpen = options?.skipSchedule ?? false;
|
||||
|
||||
// Native platforms: Skip web push initialization
|
||||
if (this.isNativePlatform) {
|
||||
@@ -689,6 +693,12 @@ export default class PushNotificationPermission extends Vue {
|
||||
"[PushNotificationPermission] Starting native notification setup",
|
||||
);
|
||||
|
||||
// Edit flow: parent will cancel + schedule; avoid double schedule (second call cancels alarm first set).
|
||||
if (this.skipScheduleForOpen) {
|
||||
this.callback(true, this.notificationTimeText, this.messageInput);
|
||||
return;
|
||||
}
|
||||
|
||||
// Import and check plugin availability before using service
|
||||
const { DailyNotification } = await import(
|
||||
"@/plugins/DailyNotificationPlugin"
|
||||
|
||||
@@ -49,6 +49,12 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
*/
|
||||
private readonly reminderId = "daily_timesafari_reminder";
|
||||
|
||||
/**
|
||||
* Ensures only one scheduleDailyNotification runs at a time (no rapid successive plugin calls).
|
||||
* Each new call waits for the previous to complete before running.
|
||||
*/
|
||||
private scheduleLock: Promise<boolean> = Promise.resolve(true);
|
||||
|
||||
/**
|
||||
* Native notifications are always supported on iOS/Android
|
||||
*/
|
||||
@@ -235,10 +241,23 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a daily notification using native alarms
|
||||
* Schedule a daily notification using native alarms.
|
||||
* Serialized so only one schedule runs at a time (avoids rapid successive plugin calls on Android).
|
||||
*/
|
||||
async scheduleDailyNotification(
|
||||
options: DailyNotificationOptions,
|
||||
): Promise<boolean> {
|
||||
const run = (): Promise<boolean> =>
|
||||
this.doScheduleDailyNotification(options);
|
||||
this.scheduleLock = this.scheduleLock.then(() => run());
|
||||
return this.scheduleLock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation of schedule; called under scheduleLock.
|
||||
*/
|
||||
private async doScheduleDailyNotification(
|
||||
options: DailyNotificationOptions,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
logger.info(
|
||||
|
||||
@@ -824,6 +824,7 @@ interface PushNotificationPermissionRef {
|
||||
open: (
|
||||
title: string,
|
||||
callback: (success: boolean, timeText: string, message?: string) => void,
|
||||
options?: { skipSchedule?: boolean },
|
||||
) => void;
|
||||
hourInput?: string;
|
||||
minuteInput?: string;
|
||||
@@ -896,6 +897,8 @@ export default class AccountViewView extends Vue {
|
||||
notifyingReminder: boolean = false;
|
||||
notifyingReminderMessage: string = "";
|
||||
notifyingReminderTime: string = "";
|
||||
/** Guard: only one edit-reminder schedule per user action (avoids double schedule on Android). */
|
||||
editReminderScheduleInProgress: boolean = false;
|
||||
subscription: PushSubscription | null = null;
|
||||
|
||||
// UI state properties
|
||||
@@ -1293,16 +1296,21 @@ export default class AccountViewView extends Vue {
|
||||
const dialog = this.$refs
|
||||
.pushNotificationPermission as PushNotificationPermission;
|
||||
|
||||
// Open the dialog - it will use the same callback pattern as showReminderNotificationChoice
|
||||
// skipSchedule: true so only this callback schedules (dialog does not). Avoids double schedule on Android.
|
||||
dialog.open(
|
||||
DIRECT_PUSH_TITLE,
|
||||
async (success: boolean, timeText: string, message?: string) => {
|
||||
if (success) {
|
||||
// Cancel the old notification before scheduling the new one
|
||||
if (!success) return;
|
||||
if (this.editReminderScheduleInProgress) return;
|
||||
this.editReminderScheduleInProgress = true;
|
||||
try {
|
||||
const service = NotificationService.getInstance();
|
||||
await service.cancelDailyNotification();
|
||||
// On iOS: cancel then schedule. On Android: plugin cancels internally when scheduling with same id; skip pre-cancel to avoid double-cancel edge cases.
|
||||
if (Capacitor.getPlatform() !== "android") {
|
||||
await service.cancelDailyNotification();
|
||||
}
|
||||
|
||||
// Schedule the updated notification
|
||||
// Schedule the updated notification (one call per user action)
|
||||
const time24h = this.parseTimeTo24Hour(timeText);
|
||||
const title = "Daily Reminder";
|
||||
const body =
|
||||
@@ -1325,8 +1333,11 @@ export default class AccountViewView extends Vue {
|
||||
this.notifyingReminderMessage = message || "";
|
||||
this.notifyingReminderTime = timeText;
|
||||
}
|
||||
} finally {
|
||||
this.editReminderScheduleInProgress = false;
|
||||
}
|
||||
},
|
||||
{ skipSchedule: true },
|
||||
);
|
||||
|
||||
// Pre-populate the form with current values after dialog opens
|
||||
|
||||
Reference in New Issue
Block a user