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

View File

@@ -13,7 +13,6 @@
import { Capacitor } from "@capacitor/core";
import type { PushNotificationSchema } from "@capacitor/push-notifications";
import { ScheduleKind } from "@timesafari/daily-notification-plugin";
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
import { REMINDER_ID_DAILY_REMINDER } from "./reminderIds";
import { configureNativeFetcherIfReady } from "./nativeFetcherConfig";
@@ -577,52 +576,54 @@ export async function refreshNotifications(): Promise<void> {
const nextNotifications = (data as { nextNotifications?: unknown })
?.nextNotifications;
// Keep existing behavior: ensure background worker credentials are current.
await configureNativeFetcherIfReady();
if (!Array.isArray(nextNotifications)) {
return;
}
// Full replacement: clear prior scheduled notifications before applying backend schedule.
// (Plugin API name is cancelAllNotifications; equivalent intent to "clearAllNotifications".)
await DailyNotification.cancelAllNotifications();
const timestamps = nextNotifications
.map((n) => (n as { timestamp?: unknown })?.timestamp)
.filter((t): t is number => typeof t === "number" && Number.isFinite(t));
// Apply backend schedule to the native scheduler as recurring notifications.
// The plugin's schedule API is cron/clockTime-based (recurring), so we map
// 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;
}
if (timestamps.length === 0) {
return;
}
const dt = new Date(timestamp);
const hh = dt.getHours().toString().padStart(2, "0");
const mm = dt.getMinutes().toString().padStart(2, "0");
const clockTime = `${hh}:${mm}`;
const id = `backend-${timestamp}`;
// Keep existing behavior: ensure background worker credentials are current.
await configureNativeFetcherIfReady();
const existing = await DailyNotification.getSchedule(id);
if (existing) {
await DailyNotification.updateSchedule(id, {
clockTime,
enabled: true,
});
} else {
// DailyNotification plugin typings currently omit `id` on CreateScheduleInput,
// but runtime supports deterministic IDs for schedule reconciliation.
await DailyNotification.createSchedule({
id,
kind: ScheduleKind.NOTIFY,
clockTime,
enabled: true,
} as unknown as never);
}
}),
);
// Backend is source of truth: apply exact timestamps (no HH:mm / recurring conversion).
// Only clear after we have a valid API response payload.
// eslint-disable-next-line no-console
console.log("[Notifications] Applying timestamps:", nextNotifications);
const plugin = DailyNotification as unknown as {
clearAllNotifications?: () => Promise<void>;
scheduleNotifications?: (options: {
timestamps: number[];
}) => Promise<void>;
cancelAllNotifications?: () => Promise<void>;
};
if (typeof plugin.clearAllNotifications === "function") {
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) {
logger.error("[NativeNotificationService] Refresh failed", err);
}