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:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user