Retire WAKEUP_PING refresh consumption and clear leftover api_* schedules on upgrade.

Stop the app from scheduling via the legacy refresh pipeline, and add a one-time Preferences-gated startup cleanup that calls clearApiNotifications() so upgraded installs shed persisted api_* residue without touching daily reminder, dual, AlertSearch, or FCM.
This commit is contained in:
Jose Olarte III
2026-09-22 20:59:23 +08:00
parent 724131d9a8
commit eb2240b901
4 changed files with 190 additions and 0 deletions
+3
View File
@@ -48,6 +48,7 @@ import {
configureNativeFetcherIfReady,
initializeNativePushAndFirebaseMessaging,
onNotificationAuthMayBeReady,
runLegacyApiNotificationsCleanupOnce,
} from "@/services/notifications";
logger.log("[Capacitor] 🚀 Starting initialization");
@@ -361,6 +362,8 @@ setTimeout(async () => {
);
await registerDeepLinkListener();
logger.info(`[Main] 🎉 Deep link system fully initialized!`);
// One-time: remove persisted legacy api_* schedules from pre-retirement installs
await runLegacyApiNotificationsCleanupOnce();
// Firebase Messaging (JS) + Capacitor PushNotifications (FCM/APNs token, delivery listeners)
await initializeNativePushAndFirebaseMessaging();
// Configure native fetcher for API-driven daily notifications (activeDid + JWT)
+1
View File
@@ -42,6 +42,7 @@ export { uploadAlertSearchAuthorization } from "./alertAuthorization";
export type { AlertAuthorizationUploadResult } from "./alertAuthorization";
export { configureNativeFetcherIfReady } from "./nativeFetcherConfig";
export { runLegacyApiNotificationsCleanupOnce } from "./legacyApiNotificationsCleanup";
export {
deferFcmRegistration,
flushDeferredFcmRegistration,
@@ -0,0 +1,101 @@
import { Capacitor } from "@capacitor/core";
import { Preferences } from "@capacitor/preferences";
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
import {
LEGACY_API_NOTIFICATIONS_CLEANUP_KEY,
runLegacyApiNotificationsCleanupOnce,
} from "./legacyApiNotificationsCleanup";
jest.mock("@capacitor/core", () => ({
Capacitor: {
isNativePlatform: jest.fn(),
},
}));
jest.mock("@capacitor/preferences", () => ({
Preferences: {
get: jest.fn(),
set: jest.fn(),
},
}));
jest.mock("@/plugins/DailyNotificationPlugin", () => ({
DailyNotification: {
clearApiNotifications: jest.fn(),
},
}));
jest.mock("@/utils/logger", () => ({
logger: {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
log: jest.fn(),
},
}));
const isNativePlatform = Capacitor.isNativePlatform as jest.Mock;
const preferencesGet = Preferences.get as jest.Mock;
const preferencesSet = Preferences.set as jest.Mock;
const clearApiNotifications =
DailyNotification.clearApiNotifications as jest.Mock;
describe("runLegacyApiNotificationsCleanupOnce", () => {
beforeEach(() => {
jest.clearAllMocks();
isNativePlatform.mockReturnValue(true);
preferencesGet.mockResolvedValue({ value: null });
preferencesSet.mockResolvedValue(undefined);
clearApiNotifications.mockResolvedValue(undefined);
});
it("no-ops on non-native platforms", async () => {
isNativePlatform.mockReturnValue(false);
await runLegacyApiNotificationsCleanupOnce();
expect(preferencesGet).not.toHaveBeenCalled();
expect(clearApiNotifications).not.toHaveBeenCalled();
expect(preferencesSet).not.toHaveBeenCalled();
});
it("skips cleanup when the migration marker is already set", async () => {
preferencesGet.mockResolvedValue({ value: "1" });
await runLegacyApiNotificationsCleanupOnce();
expect(clearApiNotifications).not.toHaveBeenCalled();
expect(preferencesSet).not.toHaveBeenCalled();
});
it("clears api_* state and writes the marker only after success", async () => {
await runLegacyApiNotificationsCleanupOnce();
expect(clearApiNotifications).toHaveBeenCalledTimes(1);
expect(preferencesSet).toHaveBeenCalledWith({
key: LEGACY_API_NOTIFICATIONS_CLEANUP_KEY,
value: "1",
});
});
it("does not write the marker when clearApiNotifications fails", async () => {
clearApiNotifications.mockRejectedValue(new Error("native failure"));
await runLegacyApiNotificationsCleanupOnce();
expect(clearApiNotifications).toHaveBeenCalledTimes(1);
expect(preferencesSet).not.toHaveBeenCalled();
});
it("does not write the marker when clearApiNotifications is missing", async () => {
const original = DailyNotification.clearApiNotifications;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (DailyNotification as any).clearApiNotifications;
await runLegacyApiNotificationsCleanupOnce();
expect(preferencesSet).not.toHaveBeenCalled();
DailyNotification.clearApiNotifications = original;
});
});
@@ -0,0 +1,85 @@
/**
* One-time upgrade cleanup for legacy API-managed notification schedules
* (`api_*`) left behind by the retired WAKEUP_PING → /notifications/refresh path.
*
* Uses DailyNotification.clearApiNotifications(), which cancels only `api_*`
* pending/delivered state (and Android persisted api_* schedule rows). It does
* not touch daily reminders, dual/New Activity schedules, AlertSearch, or FCM.
*/
import { Capacitor } from "@capacitor/core";
import { Preferences } from "@capacitor/preferences";
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
import { logger } from "@/utils/logger";
/** Preferences key; written only after a successful native cleanup. */
export const LEGACY_API_NOTIFICATIONS_CLEANUP_KEY =
"legacy_api_notifications_cleanup_v1";
const DONE_VALUE = "1";
let inFlight: Promise<void> | null = null;
/**
* Idempotent startup hook. Safe when zero `api_*` schedules exist.
* On failure, leaves the marker unset so a later launch can retry.
*/
export async function runLegacyApiNotificationsCleanupOnce(): Promise<void> {
if (inFlight) {
return inFlight;
}
inFlight = runCleanup().finally(() => {
inFlight = null;
});
return inFlight;
}
async function runCleanup(): Promise<void> {
if (!Capacitor.isNativePlatform()) {
return;
}
try {
const existing = await Preferences.get({
key: LEGACY_API_NOTIFICATIONS_CLEANUP_KEY,
});
if (existing.value === DONE_VALUE) {
return;
}
} catch (error) {
logger.warn(
"[legacyApiNotificationsCleanup] Could not read migration marker; skipping this launch",
error,
);
return;
}
const clearApiNotifications = (
DailyNotification as {
clearApiNotifications?: () => Promise<void>;
}
).clearApiNotifications;
if (typeof clearApiNotifications !== "function") {
logger.warn(
"[legacyApiNotificationsCleanup] clearApiNotifications unavailable; will retry on a later launch",
);
return;
}
try {
await clearApiNotifications.call(DailyNotification);
await Preferences.set({
key: LEGACY_API_NOTIFICATIONS_CLEANUP_KEY,
value: DONE_VALUE,
});
logger.info(
"[legacyApiNotificationsCleanup] Cleared legacy api_* notification state",
);
} catch (error) {
logger.warn(
"[legacyApiNotificationsCleanup] Cleanup failed; will retry on next startup",
error,
);
}
}