fix(notifications): apply backend timestamps via scheduleNotifications API

Stop converting backend timestamps to HH:mm/recurring schedules and remove
createSchedule/updateSchedule reconciliation. After a successful refresh payload,
clear existing notifications and schedule exact timestamps via the plugin
scheduleNotifications API (with back-compat clear fallback) to prevent drift.
This commit is contained in:
Jose Olarte III
2026-05-06 17:56:55 +08:00
parent 6bbade2a29
commit 320e55912b
@@ -13,7 +13,6 @@
import { Capacitor } from "@capacitor/core"; import { Capacitor } from "@capacitor/core";
import type { PushNotificationSchema } from "@capacitor/push-notifications"; import type { PushNotificationSchema } from "@capacitor/push-notifications";
import { ScheduleKind } from "@timesafari/daily-notification-plugin";
import { DailyNotification } from "@/plugins/DailyNotificationPlugin"; import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
import { REMINDER_ID_DAILY_REMINDER } from "./reminderIds"; import { REMINDER_ID_DAILY_REMINDER } from "./reminderIds";
import { configureNativeFetcherIfReady } from "./nativeFetcherConfig"; import { configureNativeFetcherIfReady } from "./nativeFetcherConfig";
@@ -577,52 +576,54 @@ export async function refreshNotifications(): Promise<void> {
const nextNotifications = (data as { nextNotifications?: unknown }) const nextNotifications = (data as { nextNotifications?: unknown })
?.nextNotifications; ?.nextNotifications;
// Keep existing behavior: ensure background worker credentials are current.
await configureNativeFetcherIfReady();
if (!Array.isArray(nextNotifications)) { if (!Array.isArray(nextNotifications)) {
return; return;
} }
// Full replacement: clear prior scheduled notifications before applying backend schedule. const timestamps = nextNotifications
// (Plugin API name is cancelAllNotifications; equivalent intent to "clearAllNotifications".) .map((n) => (n as { timestamp?: unknown })?.timestamp)
await DailyNotification.cancelAllNotifications(); .filter((t): t is number => typeof t === "number" && Number.isFinite(t));
// Apply backend schedule to the native scheduler as recurring notifications. if (timestamps.length === 0) {
// The plugin's schedule API is cron/clockTime-based (recurring), so we map return;
// the backend timestamps to clockTime (HH:mm) schedules. }
await Promise.all(
nextNotifications.map(async (n) => {
const timestamp = (n as { timestamp?: unknown })?.timestamp;
if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
return;
}
const dt = new Date(timestamp); // Keep existing behavior: ensure background worker credentials are current.
const hh = dt.getHours().toString().padStart(2, "0"); await configureNativeFetcherIfReady();
const mm = dt.getMinutes().toString().padStart(2, "0");
const clockTime = `${hh}:${mm}`;
const id = `backend-${timestamp}`;
const existing = await DailyNotification.getSchedule(id); // Backend is source of truth: apply exact timestamps (no HH:mm / recurring conversion).
if (existing) { // Only clear after we have a valid API response payload.
await DailyNotification.updateSchedule(id, { // eslint-disable-next-line no-console
clockTime, console.log("[Notifications] Applying timestamps:", nextNotifications);
enabled: true,
}); const plugin = DailyNotification as unknown as {
} else { clearAllNotifications?: () => Promise<void>;
// DailyNotification plugin typings currently omit `id` on CreateScheduleInput, scheduleNotifications?: (options: {
// but runtime supports deterministic IDs for schedule reconciliation. timestamps: number[];
await DailyNotification.createSchedule({ }) => Promise<void>;
id, cancelAllNotifications?: () => Promise<void>;
kind: ScheduleKind.NOTIFY, };
clockTime,
enabled: true, if (typeof plugin.clearAllNotifications === "function") {
} as unknown as never); await plugin.clearAllNotifications();
} } else if (typeof plugin.cancelAllNotifications === "function") {
}), // Back-compat: older builds expose cancelAllNotifications.
); await plugin.cancelAllNotifications();
} else {
logger.warn(
"[NativeNotificationService] No clearAllNotifications/cancelAllNotifications on plugin; cannot replace schedule",
);
return;
}
if (typeof plugin.scheduleNotifications !== "function") {
logger.warn(
"[NativeNotificationService] scheduleNotifications not available on plugin; cannot apply timestamps",
);
return;
}
await plugin.scheduleNotifications({ timestamps });
} catch (err) { } catch (err) {
logger.error("[NativeNotificationService] Refresh failed", err); logger.error("[NativeNotificationService] Refresh failed", err);
} }