WIP: new-activity notification #231

Draft
trentlarson wants to merge 80 commits from notify-api into master
9 changed files with 158 additions and 2 deletions
Showing only changes of commit 5bc030125a - Show all commits

View File

@@ -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 T5 fetch without this).
await configureNativeFetcherIfReady();
onNotificationAuthMayBeReady();
}
});
}

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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)";

View 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");
}

View File

@@ -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();
}
}
/**

View File

@@ -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}:`,