diff --git a/src/components/dev/NotificationDebugPanel.vue b/src/components/dev/NotificationDebugPanel.vue index 63b40a30..2b2d61d1 100644 --- a/src/components/dev/NotificationDebugPanel.vue +++ b/src/components/dev/NotificationDebugPanel.vue @@ -88,25 +88,6 @@ Manually mints and uploads 100 delegated day JWTs. Requires an active did:ethr identity and JWT authentication; Test Mode is not used.

- - -

- Local simulation only — calls the refresh API directly (no FCM push). -

- -
- Selected interval: {{ intervalLabel }} -
- - - -
-

Mock Refresh Controls

- -
- - -
-

Wakeup Ping Simulator

-

- Exercises the production push handler (not the refresh API shortcut - above). -

- -
- - -
-

Flood Test

- -
-
@@ -358,14 +275,6 @@ type PendingInfo = { wallClockSource?: string | null; }; -const presets = [ - { label: "30 sec", ms: 30_000 }, - { label: "1 min", ms: 60_000 }, - { label: "5 min", ms: 5 * 60_000 }, - { label: "10 min", ms: 10 * 60_000 }, -]; - -const intervalMs = ref(60_000); const busy = ref(false); const pending = ref([]); const pendingInspectorMessage = ref(null); @@ -395,11 +304,6 @@ const truncatedFcmToken = computed(() => { const eventLog = ref([]); let unsubscribeEventLog: (() => void) | undefined; -const intervalLabel = computed(() => { - const preset = presets.find((p) => p.ms === intervalMs.value); - return preset?.label ?? `${intervalMs.value}ms`; -}); - function formatIsoMs(ms: number | null | undefined): string { if (ms == null || !Number.isFinite(ms)) { return ""; @@ -423,27 +327,6 @@ async function refreshPending(): Promise { pendingInspectorMessage.value = result.inspectorUnavailableMessage ?? null; } -async function onMockRefresh(): Promise { - await withBusy(async () => { - await NotificationDebugService.triggerMockRefresh(intervalMs.value); - await refreshPending(); - }); -} - -async function onWakeupPing(): Promise { - await withBusy(async () => { - await NotificationDebugService.simulateWakeupPing(); - await refreshPending(); - }); -} - -async function onFloodTest(): Promise { - await withBusy(async () => { - await NotificationDebugService.runFloodTest(intervalMs.value); - await refreshPending(); - }); -} - async function onClearNotifications(): Promise { await withBusy(async () => { await NotificationDebugService.clearNotifications(); @@ -514,20 +397,6 @@ async function onUploadAlertAuthorization(): Promise { }); } -async function onBackendRefresh(): Promise { - await withBusy(async () => { - await NotificationDebugService.triggerBackendRefresh(); - await refreshPending(); - }); -} - -async function onSimulateWakeupRefresh(): Promise { - await withBusy(async () => { - await NotificationDebugService.simulateWakeupViaRefresh(); - await refreshPending(); - }); -} - function formatRealWakeupStatusMessage( result: Awaited< ReturnType diff --git a/src/composables/useNotifications.ts b/src/composables/useNotifications.ts deleted file mode 100644 index 2f04ec59..00000000 --- a/src/composables/useNotifications.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { inject, onBeforeUnmount, onMounted } from "vue"; -import { NotificationIface } from "../constants/app"; -import { registerToken } from "@/services/notifications/NotificationService"; -import { refreshNotifications } from "@/services/notifications/NativeNotificationService"; - -/** - * Vue 3 composable for notifications - * Provides a concise API for common notification patterns - */ - -export const NOTIFICATION_TIMEOUTS = { - BRIEF: 1000, // Very brief toasts ("Sent..." messages) - SHORT: 2000, // Short notifications (clipboard copies, quick confirmations) - STANDARD: 3000, // Standard notifications (success messages, general info) - LONG: 5000, // Longer notifications (errors, warnings, important info) - VERY_LONG: 7000, // Very long notifications (complex operations) - MODAL: -1, // Modal confirmations (no auto-dismiss) -} as const; - -export function useNotifications() { - // Inject the notify function from the app - const notify = - inject<(notification: NotificationIface, timeout?: number) => void>( - "notify", - ); - - if (!notify) { - throw new Error( - "useNotifications must be used within a component that has $notify available", - ); - } - - let refreshTimer: number | undefined = undefined; - let refreshInFlight: Promise | null = null; - - async function refreshNotificationsDebounced(): Promise { - if (refreshTimer != null) { - window.clearTimeout(refreshTimer); - } - refreshTimer = window.setTimeout(() => { - if (!refreshInFlight) { - refreshInFlight = refreshNotifications().finally(() => { - refreshInFlight = null; - }); - } - }, 300); - } - - const onResume = () => { - void refreshNotificationsDebounced(); - }; - - onMounted(() => { - void refreshNotificationsDebounced(); - document.addEventListener("resume", onResume); - }); - - onBeforeUnmount(() => { - document.removeEventListener("resume", onResume); - if (refreshTimer != null) { - window.clearTimeout(refreshTimer); - } - }); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function success(_notification: NotificationIface, _timeout?: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function error(_notification: NotificationIface, _timeout?: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function warning(_notification: NotificationIface, _timeout?: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function info(_notification: NotificationIface, _timeout?: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function toast(_title: string, _text?: string, _timeout?: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function copied(_item: string, _timeout?: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function sent(_timeout?: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function confirm( - _text: string, - _onYes: () => Promise, - _timeout?: number, - ) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function confirmationSubmitted(_timeout?: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function genericError(_timeout?: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function genericSuccess(_timeout?: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function alreadyConfirmed(_timeout?: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function cannotConfirmIssuer(_timeout?: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function cannotConfirmHidden(_timeout?: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function notRegistered(_timeout?: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function notAGive(_timeout?: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function notificationOff( - _title: string, - _callback: (success: boolean) => Promise, - _timeout?: number, - ) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function downloadStarted(_format: string = "Dexie", _timeout?: number) {} - - return { - success, - error, - warning, - info, - toast, - copied, - sent, - confirm, - confirmationSubmitted, - genericError, - genericSuccess, - alreadyConfirmed, - cannotConfirmIssuer, - cannotConfirmHidden, - notRegistered, - notAGive, - notificationOff, - downloadStarted, - /** POST FCM token to `/notifications/register` (same as startup native hook). */ - registerFcmToken: registerToken, - refreshNotifications: refreshNotificationsDebounced, - }; -} diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 82bc1395..e259129b 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -23,7 +23,7 @@ import { LRUCache } from "lru-cache"; import * as R from "ramda"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app"; -import { NOTIFICATION_TIMEOUTS } from "../composables/useNotifications"; +import { NOTIFICATION_TIMEOUTS } from "../utils/notificationUtils"; import { createNotifyHelpers } from "../utils/notify"; import { NOTIFY_PERSONAL_DATA_ERROR } from "../constants/notifications"; import { Contact } from "../db/tables/contacts"; diff --git a/src/services/notifications/NativeNotificationService.ts b/src/services/notifications/NativeNotificationService.ts index 418802e0..9e02000b 100644 --- a/src/services/notifications/NativeNotificationService.ts +++ b/src/services/notifications/NativeNotificationService.ts @@ -13,30 +13,8 @@ import { Capacitor } from "@capacitor/core"; import type { PushNotificationSchema } from "@capacitor/push-notifications"; -import type { - NotificationRefreshRequest, - NotificationRefreshResponse, -} from "@/interfaces/notifyApi"; import { DailyNotification } from "@/plugins/DailyNotificationPlugin"; -import { getOrCreateDeviceId } from "./deviceId"; import { REMINDER_ID_DAILY_REMINDER } from "./reminderIds"; -import { configureNativeFetcherIfReady } from "./nativeFetcherConfig"; -import { - getNotificationApiBaseUrl, - getTestMode, -} from "./NotificationDebugConfig"; -import { - logRefreshFailure, - logRefreshStarted, - logRefreshSuccess, - logScheduleReplacement, -} from "./notificationLog"; -import { - getNotificationApiHeaders, - logSkippingRefreshDueToMissingAuth, - notificationApiFailureMessage, - readNotificationApiBody, -} from "./notificationApiAuth"; import { logNotification } from "./NotificationDebugEvents"; /** @@ -563,200 +541,14 @@ export class NativeNotificationService implements NotificationServiceInterface { } } -export type RefreshNotificationsResult = { - ok: boolean; - scheduledCount: number; - status?: number; - errorMessage?: string; -}; - /** - * Re-applies native API fetcher credentials (JWT pool, active DID) so background - * notification workers can run. No UI; safe from push handlers while backgrounded. - */ -export async function refreshNotificationsWithDiagnostics(options?: { - source?: string; -}): Promise { - const startedAt = performance.now(); - const source = options?.source; - logRefreshStarted(source); - - if (!Capacitor.isNativePlatform()) { - const errorMessage = "not a native platform"; - logRefreshFailure(startedAt, errorMessage, undefined, source); - return { - ok: false, - scheduledCount: 0, - errorMessage, - }; - } - - try { - const auth = await getNotificationApiHeaders("refresh"); - if (!auth.ok) { - logSkippingRefreshDueToMissingAuth(); - logRefreshFailure(startedAt, auth.message, undefined, source); - return { - ok: false, - scheduledCount: 0, - errorMessage: auth.message, - }; - } - - let deviceId: string | undefined; - try { - deviceId = await getOrCreateDeviceId(); - } catch (err) { - logger.warn( - "[NativeNotificationService] Could not obtain deviceId; skipping refresh", - err, - ); - } - if (!deviceId) { - // The service finds the device by deviceId or fcmToken and answers 400 - // without either, so there is no request worth sending. - const errorMessage = "no deviceId (cannot identify this device)"; - logRefreshFailure(startedAt, errorMessage, undefined, source); - return { ok: false, scheduledCount: 0, errorMessage }; - } - - const body: NotificationRefreshRequest = { - deviceId, - platform: Capacitor.getPlatform(), - testMode: getTestMode(), - }; - const baseUrl = getNotificationApiBaseUrl(); - const res = await fetch(`${baseUrl}/notifications/refresh`, { - method: "POST", - headers: auth.headers, - body: JSON.stringify(body), - }); - - if (!res.ok) { - const errorMessage = notificationApiFailureMessage( - res.status, - await readNotificationApiBody(res), - ); - logger.warn("[NativeNotificationService] refreshNotifications failed", { - status: res.status, - statusText: res.statusText, - errorMessage, - }); - logRefreshFailure(startedAt, errorMessage, res.status, source); - return { - ok: false, - scheduledCount: 0, - status: res.status, - errorMessage, - }; - } - - const payload = (await res.json()) as NotificationRefreshResponse; - const scheduledCount = Array.isArray(payload?.nextNotifications) - ? payload.nextNotifications.length - : 0; - await applyNotificationRefreshPayload(payload); - logRefreshSuccess(startedAt, scheduledCount, source); - return { ok: true, scheduledCount }; - } catch (err) { - logger.error("[NativeNotificationService] Refresh failed", err); - const message = err instanceof Error ? err.message : String(err); - logRefreshFailure(startedAt, message, undefined, source); - return { ok: false, scheduledCount: 0, errorMessage: message }; - } -} - -export async function refreshNotifications(): Promise { - await refreshNotificationsWithDiagnostics(); -} - -export type NotificationRefreshPayload = { - shouldNotify?: boolean; - nextNotifications?: Array<{ timestamp?: number }>; -}; - -// `handleCapacitorPushNotificationReceived` and `applyNotificationRefreshPayload` are used by -// DEV notification simulation tooling; they must stay production-safe because that tooling -// exercises real flows. (`applyNotificationRefreshPayload` is also used by production refresh.) - -/** - * Apply a "refresh notifications" payload by clearing and scheduling timestamps via the native plugin. - * - * This is the shared implementation used by: - * - production refresh flow (`refreshNotifications` fetching from backend) - * - dev-only debug flows (mock refresh with local payloads) - * - * Important: This function intentionally mirrors production behavior and does not introduce - * any scheduling logic in UI layers. - */ -export async function applyNotificationRefreshPayload( - payload: unknown, -): Promise { - if (!Capacitor.isNativePlatform()) { - return; - } - - const data = payload as NotificationRefreshPayload; - const nextNotifications = data?.nextNotifications; - if (!Array.isArray(nextNotifications)) { - return; - } - - const timestamps = nextNotifications - .map((n) => (n as { timestamp?: unknown })?.timestamp) - .filter((t): t is number => typeof t === "number" && Number.isFinite(t)); - - if (timestamps.length === 0) { - logNotification("Schedule replacement skipped (no valid timestamps)"); - return; - } - - // Keep existing behavior: ensure background worker credentials are current. - await configureNativeFetcherIfReady(); - - logScheduleReplacement(timestamps.length); - - if (typeof DailyNotification.clearApiNotifications !== "function") { - logger.warn( - "[NativeNotificationService] API notification clear unavailable (plugin clearApiNotifications missing); cannot replace schedule", - ); - logNotification( - "Schedule replacement aborted (API notification clear unavailable on plugin)", - ); - return; - } - - logNotification("Clearing API notifications before refresh"); - await DailyNotification.clearApiNotifications(); - logNotification("Cleared API notifications"); - - if (typeof DailyNotification.scheduleApiNotifications !== "function") { - logger.warn( - "[NativeNotificationService] scheduleApiNotifications not available on plugin; cannot apply timestamps", - ); - logNotification( - "Schedule replacement aborted (scheduleApiNotifications unavailable)", - ); - return; - } - - await DailyNotification.scheduleApiNotifications({ timestamps }); - logNotification( - `Schedule replacement applied (${timestamps.length} timestamp(s))`, - ); -} - -/** - * Silent FCM/APNs data push: refresh native notification pipeline when requested by backend. + * Capacitor push delivery hook. Legacy WAKEUP_PING → /notifications/refresh + * consumption was retired; AlertSearch uses visible FCM notifications and is + * not handled here. Keep logging for diagnostics without scheduling api_* work. */ export async function handleCapacitorPushNotificationReceived( notification: PushNotificationSchema, ): Promise { - if (notification.data?.type === "WAKEUP_PING") { - logNotification("WAKEUP_PING handler — invoking refresh"); - await refreshNotificationsWithDiagnostics({ source: "WAKEUP_PING" }); - return; - } const type = typeof notification.data?.type === "string" ? notification.data.type diff --git a/src/services/notifications/NotificationDebugService.ts b/src/services/notifications/NotificationDebugService.ts index 93c514de..f01229e7 100644 --- a/src/services/notifications/NotificationDebugService.ts +++ b/src/services/notifications/NotificationDebugService.ts @@ -1,14 +1,12 @@ /** * DEV-only notification testing utilities. * - * IMPORTANT: - * This service intentionally routes through the same production notification - * orchestration paths used by refresh flows, wakeup pushes, and replacement. - * Avoid adding duplicate scheduling logic here. + * Legacy WAKEUP_PING → /notifications/refresh → api_* tooling was removed in + * the Phase 2 retirement. Remaining helpers cover FCM registration, AlertSearch + * authorization upload, backend URL overrides, and pending-notification inspection. */ import { Capacitor } from "@capacitor/core"; -import type { PushNotificationSchema } from "@capacitor/push-notifications"; import type { DebugSendWakeupRequest, DebugSendWakeupResponse, @@ -38,12 +36,6 @@ import { notificationApiFailureMessage, readNotificationApiBody, } from "./notificationApiAuth"; -import { - applyNotificationRefreshPayload, - handleCapacitorPushNotificationReceived, - refreshNotificationsWithDiagnostics, - type NotificationRefreshPayload, -} from "./NativeNotificationService"; import { truncateFcmTokenForLog } from "./notificationLog"; import { DailyNotification } from "@/plugins/DailyNotificationPlugin"; import { NotificationInspector } from "@/plugins/NotificationInspectorPlugin"; @@ -179,19 +171,10 @@ export const NotificationDebugService = { return result; }, - async triggerBackendRefresh(): Promise { - await refreshNotificationsWithDiagnostics({ source: "debug panel" }); - }, - - /** Local simulation: same API call as a WAKEUP_PING handler (no push payload). */ - async simulateWakeupViaRefresh(): Promise { - logNotification("WAKEUP_PING simulation (local refresh API only)"); - await refreshNotificationsWithDiagnostics({ - source: "WAKEUP_PING simulation", - }); - }, - - /** Full pipeline: backend `/debug/send-wakeup` → FCM → native WAKEUP_PING handler. */ + /** + * Backend `/debug/send-wakeup` → FCM only. App no longer consumes WAKEUP_PING + * for api_* scheduling; kept for FCM delivery diagnostics until backend Phase 3. + */ async sendRealWakeupPing(): Promise { logNotification("Real WAKEUP_PING requested"); @@ -264,62 +247,6 @@ export const NotificationDebugService = { } }, - generateMockNotifications( - intervalMs: number = 60_000, - ): NotificationRefreshPayload { - const now = Date.now(); - const future1 = now + intervalMs; - const future2 = now + intervalMs * 2; - - return { - shouldNotify: true, - nextNotifications: [{ timestamp: future1 }, { timestamp: future2 }], - }; - }, - - async triggerMockRefresh(intervalMs?: number): Promise { - logNotification("Mock refresh requested"); - - const payload = this.generateMockNotifications(intervalMs); - const timestamps = payload.nextNotifications?.map((n) => n.timestamp) ?? []; - logNotification(`Mock payload generated (${timestamps.length} timestamps)`); - - if (!Capacitor.isNativePlatform()) { - logNotification("Mock refresh skipped: not running on native platform"); - return; - } - - await applyNotificationRefreshPayload(payload); - logNotification("Mock refresh applied"); - }, - - async simulateWakeupPing(): Promise { - logNotification("Simulating WAKEUP_PING (production push handler)"); - - if (!Capacitor.isNativePlatform()) { - logNotification("WAKEUP_PING simulation skipped: not native platform"); - return; - } - - const notification = { - title: "WAKEUP_PING", - body: "", - id: "dev_wakeup_ping", - data: { type: "WAKEUP_PING" }, - } as unknown as PushNotificationSchema; - - await handleCapacitorPushNotificationReceived(notification); - }, - - async runFloodTest(intervalMs?: number): Promise { - logNotification("Flood test started (20 sequential refreshes)"); - for (let i = 0; i < 20; i++) { - logNotification(`Flood iteration ${i + 1}/20`); - await this.triggerMockRefresh(intervalMs); - } - logNotification("Flood test completed"); - }, - async clearNotifications(): Promise { logNotification("Clear notifications (debug panel)"); diff --git a/src/services/notifications/notificationLog.ts b/src/services/notifications/notificationLog.ts index d8dc1298..6b7d2878 100644 --- a/src/services/notifications/notificationLog.ts +++ b/src/services/notifications/notificationLog.ts @@ -24,9 +24,6 @@ export function logPushNotificationReceived(notification: { title: notification.title, dataType: type, }); - if (type === "WAKEUP_PING") { - logNotification("WAKEUP_PING received — will trigger refresh"); - } } export function logPushNotificationActionPerformed(action: { @@ -74,44 +71,6 @@ export function logTokenRegistrationFailure( }); } -export function logRefreshStarted(source?: string): void { - logNotification(source ? `Refresh started (${source})` : "Refresh started"); -} - -function elapsedMsSince(startedAt: number): number { - return performance.now() - startedAt; -} - -export function logRefreshSuccess( - startedAt: number, - scheduledCount: number, - source?: string, -): void { - const elapsedMs = Math.round(elapsedMsSince(startedAt)); - const message = source - ? `Refresh completed (${source}) in ${elapsedMs}ms (scheduled ${scheduledCount})` - : `Refresh completed in ${elapsedMs}ms (scheduled ${scheduledCount})`; - logNotification(message); -} - -export function logRefreshFailure( - startedAt: number, - errorMessage: string, - status?: number, - source?: string, -): void { - const statusPart = status != null ? ` HTTP ${status}` : ""; - const elapsedMs = Math.round(elapsedMsSince(startedAt)); - const message = source - ? `Refresh failed (${source}) in ${elapsedMs}ms: ${errorMessage}${statusPart}` - : `Refresh failed in ${elapsedMs}ms: ${errorMessage}${statusPart}`; - logNotification(message); -} - export function logNotificationClearing(method: string): void { logNotification(`Clearing notifications via ${method}`); } - -export function logScheduleReplacement(count: number): void { - logNotification(`Schedule replacement: ${count} notification(s)`); -}