WIP: new-activity notification #231
@@ -46,6 +46,7 @@ import "@timesafari/daily-notification-plugin";
|
||||
import {
|
||||
configureNativeFetcherIfReady,
|
||||
initializeNativePushAndFirebaseMessaging,
|
||||
onNotificationAuthMayBeReady,
|
||||
} from "@/services/notifications";
|
||||
|
||||
logger.log("[Capacitor] 🚀 Starting initialization");
|
||||
@@ -465,6 +466,7 @@ if (
|
||||
// Refresh JWT for background New Activity prefetch (WorkManager cannot run JS;
|
||||
// short-lived tokens would expire between configure and T−5 fetch without this).
|
||||
await configureNativeFetcherIfReady();
|
||||
onNotificationAuthMayBeReady();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ import {
|
||||
import {
|
||||
getNotificationApiHeaders,
|
||||
httpAuthErrorMessage,
|
||||
logNotificationAuthFailure,
|
||||
logNotificationRequestAuthenticated,
|
||||
logSkippingRefreshDueToMissingAuth,
|
||||
} from "./notificationApiAuth";
|
||||
import { logNotification } from "./NotificationDebugEvents";
|
||||
|
||||
@@ -593,7 +593,7 @@ export async function refreshNotificationsWithDiagnostics(options?: {
|
||||
try {
|
||||
const auth = await getNotificationApiHeaders();
|
||||
if (!auth.ok) {
|
||||
logNotificationAuthFailure("refresh", auth.message);
|
||||
logSkippingRefreshDueToMissingAuth();
|
||||
logRefreshFailure(startedAt, auth.message);
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
} from "firebase/messaging";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { handleCapacitorPushNotificationReceived } from "./NativeNotificationService";
|
||||
import { getNotificationApiHeaders } from "./notificationApiAuth";
|
||||
import { deferFcmRegistration } from "./notificationAuthLifecycle";
|
||||
import { registerToken } from "./NotificationService";
|
||||
import {
|
||||
logPushNotificationActionPerformed,
|
||||
@@ -51,6 +53,16 @@ async function registerRetrievedToken(
|
||||
logTokenRegistrationSkippedDuplicate(trimmed);
|
||||
return;
|
||||
}
|
||||
|
||||
const auth = await getNotificationApiHeaders();
|
||||
if (!auth.ok) {
|
||||
if (options?.force) {
|
||||
throw new Error(`FCM registration auth unavailable: ${auth.message}`);
|
||||
}
|
||||
deferFcmRegistration(trimmed);
|
||||
return;
|
||||
}
|
||||
|
||||
await registerToken(trimmed);
|
||||
lastRegisteredFcmToken = trimmed;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,11 @@ export { NativeNotificationService } from "./NativeNotificationService";
|
||||
export { WebPushNotificationService } from "./WebPushNotificationService";
|
||||
|
||||
export { configureNativeFetcherIfReady } from "./nativeFetcherConfig";
|
||||
export {
|
||||
deferFcmRegistration,
|
||||
flushDeferredFcmRegistration,
|
||||
onNotificationAuthMayBeReady,
|
||||
} from "./notificationAuthLifecycle";
|
||||
export {
|
||||
ensureFirebaseApp,
|
||||
initializeNativePushAndFirebaseMessaging,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { mintBackgroundJwtTokenPool } from "@/libs/crypto";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
||||
import { onNotificationAuthMayBeReady } from "./notificationAuthLifecycle";
|
||||
|
||||
/**
|
||||
* Configure the native notification content fetcher with API credentials.
|
||||
@@ -90,6 +91,7 @@ export async function configureNativeFetcherIfReady(
|
||||
jwtTokens.length +
|
||||
")",
|
||||
);
|
||||
onNotificationAuthMayBeReady();
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("[nativeFetcherConfig] configureNativeFetcher failed:", error);
|
||||
|
||||
@@ -93,6 +93,14 @@ export function logNotificationAuthFailure(
|
||||
logNotification(`${verb} auth unavailable: ${message}`);
|
||||
}
|
||||
|
||||
export function logWaitingForAuthBeforeRegistration(): void {
|
||||
logNotification("Waiting for auth before registration");
|
||||
}
|
||||
|
||||
export function logSkippingRefreshDueToMissingAuth(): void {
|
||||
logNotification("Skipping refresh due to missing auth");
|
||||
}
|
||||
|
||||
export function httpAuthErrorMessage(status: number): string {
|
||||
if (status === 401) {
|
||||
return "unauthorized (expired or invalid auth)";
|
||||
|
||||
115
src/services/notifications/notificationAuthLifecycle.ts
Normal file
115
src/services/notifications/notificationAuthLifecycle.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Defers notification register/refresh until app auth (active DID + Bearer) is available.
|
||||
* Bounded retries avoid racing startup and prevent infinite loops.
|
||||
*/
|
||||
|
||||
import { logger } from "@/utils/logger";
|
||||
import {
|
||||
getNotificationApiHeaders,
|
||||
logWaitingForAuthBeforeRegistration,
|
||||
} from "./notificationApiAuth";
|
||||
import { registerToken } from "./NotificationService";
|
||||
|
||||
const MAX_REGISTER_RETRY_ATTEMPTS = 6;
|
||||
const REGISTER_RETRY_BASE_MS = 2_000;
|
||||
const REGISTER_RETRY_MAX_MS = 60_000;
|
||||
|
||||
let pendingFcmToken: string | null = null;
|
||||
let registerRetryAttempt = 0;
|
||||
let registerRetryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let registerFlushInFlight: Promise<void> | null = null;
|
||||
|
||||
function clearRegisterRetryTimer(): void {
|
||||
if (registerRetryTimer != null) {
|
||||
clearTimeout(registerRetryTimer);
|
||||
registerRetryTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleDeferredRegistrationRetry(): void {
|
||||
if (!pendingFcmToken || registerRetryTimer != null) {
|
||||
return;
|
||||
}
|
||||
if (registerRetryAttempt >= MAX_REGISTER_RETRY_ATTEMPTS) {
|
||||
logger.warn(
|
||||
"[notificationAuthLifecycle] Stopped retrying deferred FCM registration (max attempts)",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const delay = Math.min(
|
||||
REGISTER_RETRY_BASE_MS * 2 ** registerRetryAttempt,
|
||||
REGISTER_RETRY_MAX_MS,
|
||||
);
|
||||
registerRetryAttempt += 1;
|
||||
registerRetryTimer = setTimeout(() => {
|
||||
registerRetryTimer = null;
|
||||
void flushDeferredFcmRegistration("scheduled retry");
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue FCM token registration until Bearer auth is available; retries with backoff.
|
||||
*/
|
||||
export function deferFcmRegistration(token: string): void {
|
||||
const trimmed = token.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
pendingFcmToken = trimmed;
|
||||
logWaitingForAuthBeforeRegistration();
|
||||
scheduleDeferredRegistrationRetry();
|
||||
}
|
||||
|
||||
/** Attempt pending FCM registration when auth may now be ready (identity, resume, fetcher config). */
|
||||
export async function flushDeferredFcmRegistration(
|
||||
reason?: string,
|
||||
): Promise<void> {
|
||||
if (!pendingFcmToken) {
|
||||
return;
|
||||
}
|
||||
if (registerFlushInFlight) {
|
||||
return registerFlushInFlight;
|
||||
}
|
||||
|
||||
registerFlushInFlight = (async () => {
|
||||
const token = pendingFcmToken;
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auth = await getNotificationApiHeaders();
|
||||
if (!auth.ok) {
|
||||
scheduleDeferredRegistrationRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
clearRegisterRetryTimer();
|
||||
registerRetryAttempt = 0;
|
||||
pendingFcmToken = null;
|
||||
|
||||
try {
|
||||
await registerToken(token);
|
||||
} catch (err) {
|
||||
pendingFcmToken = token;
|
||||
logger.warn(
|
||||
`[notificationAuthLifecycle] Deferred FCM registration failed${reason ? ` (${reason})` : ""}`,
|
||||
err,
|
||||
);
|
||||
scheduleDeferredRegistrationRetry();
|
||||
}
|
||||
})().finally(() => {
|
||||
registerFlushInFlight = null;
|
||||
});
|
||||
|
||||
return registerFlushInFlight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when active identity or session may have become available (additive hooks only).
|
||||
*/
|
||||
export function onNotificationAuthMayBeReady(): void {
|
||||
registerRetryAttempt = 0;
|
||||
clearRegisterRetryTimer();
|
||||
void flushDeferredFcmRegistration("auth may be ready");
|
||||
}
|
||||
@@ -139,6 +139,12 @@ export abstract class BaseDatabaseService {
|
||||
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
|
||||
[did],
|
||||
);
|
||||
if (did?.trim()) {
|
||||
const { onNotificationAuthMayBeReady } = await import(
|
||||
"@/services/notifications/notificationAuthLifecycle"
|
||||
);
|
||||
onNotificationAuthMayBeReady();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -231,6 +231,12 @@ export const PlatformServiceMixin = {
|
||||
logger.debug(
|
||||
`[PlatformServiceMixin] ActiveDid updated in active_identity table: ${newDid}`,
|
||||
);
|
||||
if (newDid) {
|
||||
const { onNotificationAuthMayBeReady } = await import(
|
||||
"@/services/notifications/notificationAuthLifecycle"
|
||||
);
|
||||
onNotificationAuthMayBeReady();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[PlatformServiceMixin] Error updating activeDid in active_identity table ${newDid}:`,
|
||||
|
||||
Reference in New Issue
Block a user