feat(notifications): register FCM tokens with backend

Add registerToken POST to /notifications/register (platform, testMode).
Call it from Capacitor registration and Firebase getToken with deduped
registerRetrievedToken; expose registerToken via barrel and useNotifications
as registerFcmToken.
This commit is contained in:
Jose Olarte III
2026-05-06 15:40:00 +08:00
parent 162158066f
commit c523c14d96
4 changed files with 49 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { inject } from "vue"; import { inject } from "vue";
import { NotificationIface } from "../constants/app"; import { NotificationIface } from "../constants/app";
import { registerToken } from "@/services/notifications/NotificationService";
/** /**
* Vue 3 composable for notifications * Vue 3 composable for notifications
@@ -93,5 +94,7 @@ export function useNotifications() {
notAGive, notAGive,
notificationOff, notificationOff,
downloadStarted, downloadStarted,
/** POST FCM token to `/notifications/register` (same as startup native hook). */
registerFcmToken: registerToken,
}; };
} }

View File

@@ -14,9 +14,32 @@
*/ */
import { Capacitor } from "@capacitor/core"; import { Capacitor } from "@capacitor/core";
import { logger } from "@/utils/logger";
import { NativeNotificationService } from "./NativeNotificationService"; import { NativeNotificationService } from "./NativeNotificationService";
import { WebPushNotificationService } from "./WebPushNotificationService"; import { WebPushNotificationService } from "./WebPushNotificationService";
/**
* Registers an FCM device token with the app backend (native Capacitor token or web getToken).
*/
export async function registerToken(fcmToken: string): Promise<void> {
const res = await fetch("/notifications/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
fcmToken,
platform: Capacitor.getPlatform(),
testMode: true,
}),
});
if (!res.ok) {
logger.warn("[NotificationService] registerToken failed", {
status: res.status,
statusText: res.statusText,
});
throw new Error(`registerToken failed: HTTP ${res.status}`);
}
}
/** /**
* Options for scheduling a daily notification * Options for scheduling a daily notification
*/ */

View File

@@ -21,11 +21,26 @@ import {
onMessage, onMessage,
} from "firebase/messaging"; } from "firebase/messaging";
import { logger } from "@/utils/logger"; import { logger } from "@/utils/logger";
import { registerToken } from "./NotificationService";
const LOG = "[FirebaseMessaging]"; const LOG = "[FirebaseMessaging]";
let firebaseAppSingleton: FirebaseApp | null = null; let firebaseAppSingleton: FirebaseApp | null = null;
let nativeInitPromise: Promise<void> | null = null; let nativeInitPromise: Promise<void> | null = null;
/** Avoid duplicate POSTs when the same token is delivered more than once. */
let lastRegisteredFcmToken: string | null = null;
async function registerRetrievedToken(token: string): Promise<void> {
const trimmed = token.trim();
if (!trimmed) {
return;
}
if (trimmed === lastRegisteredFcmToken) {
return;
}
await registerToken(trimmed);
lastRegisteredFcmToken = trimmed;
}
function readFirebaseOptions(): FirebaseOptions | null { function readFirebaseOptions(): FirebaseOptions | null {
const env = import.meta.env; const env = import.meta.env;
@@ -107,6 +122,7 @@ async function attachFirebaseMessagingIfSupported(
logger.info(`${LOG} Firebase getToken completed`, { logger.info(`${LOG} Firebase getToken completed`, {
tokenPrefix: token ? `${token.slice(0, 12)}` : "(empty)", tokenPrefix: token ? `${token.slice(0, 12)}` : "(empty)",
}); });
await registerRetrievedToken(token);
} catch (err) { } catch (err) {
logger.warn( logger.warn(
`${LOG} Firebase getToken failed (common on native WebView without SW)`, `${LOG} Firebase getToken failed (common on native WebView without SW)`,
@@ -135,6 +151,12 @@ async function initializeNativePushAndFirebaseMessagingImpl(): Promise<void> {
logger.info(`${LOG} Capacitor registration token`, { logger.info(`${LOG} Capacitor registration token`, {
valuePrefix: token.value ? `${token.value.slice(0, 12)}` : "(empty)", valuePrefix: token.value ? `${token.value.slice(0, 12)}` : "(empty)",
}); });
void registerRetrievedToken(token.value).catch((err) => {
logger.warn(
`${LOG} registerToken after Capacitor registration failed`,
err,
);
});
}); });
await PushNotifications.addListener("registrationError", (err) => { await PushNotifications.addListener("registrationError", (err) => {

View File

@@ -13,7 +13,7 @@
* ``` * ```
*/ */
export { NotificationService } from "./NotificationService"; export { NotificationService, registerToken } from "./NotificationService";
export { NativeNotificationService } from "./NativeNotificationService"; export { NativeNotificationService } from "./NativeNotificationService";
export { WebPushNotificationService } from "./WebPushNotificationService"; export { WebPushNotificationService } from "./WebPushNotificationService";