feat(notifications): initialize Firebase Messaging and Capacitor push on native
Add firebaseMessagingClient to ensure the Firebase app is created from VITE_FIREBASE_*, wire PushNotifications (listeners, requestPermissions, register) before token work, and call getMessaging/getToken/onMessage when firebase/messaging is supported. Hook startup from main.capacitor and set PushNotifications presentationOptions in capacitor.config. Depend on firebase and @capacitor/push-notifications.
This commit is contained in:
@@ -15,6 +15,7 @@ dependencies {
|
||||
implementation project(':capacitor-camera')
|
||||
implementation project(':capacitor-clipboard')
|
||||
implementation project(':capacitor-filesystem')
|
||||
implementation project(':capacitor-push-notifications')
|
||||
implementation project(':capacitor-share')
|
||||
implementation project(':capacitor-status-bar')
|
||||
implementation project(':capawesome-capacitor-file-picker')
|
||||
|
||||
@@ -16,6 +16,13 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"PushNotifications": {
|
||||
"presentationOptions": [
|
||||
"badge",
|
||||
"sound",
|
||||
"alert"
|
||||
]
|
||||
},
|
||||
"SplashScreen": {
|
||||
"launchShowDuration": 3000,
|
||||
"launchAutoHide": true,
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"pkg": "@capacitor/filesystem",
|
||||
"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/push-notifications",
|
||||
"classpath": "com.capacitorjs.plugins.pushnotifications.PushNotificationsPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/share",
|
||||
"classpath": "com.capacitorjs.plugins.share.SharePlugin"
|
||||
|
||||
@@ -20,6 +20,9 @@ project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacito
|
||||
include ':capacitor-filesystem'
|
||||
project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
|
||||
|
||||
include ':capacitor-push-notifications'
|
||||
project(':capacitor-push-notifications').projectDir = new File('../node_modules/@capacitor/push-notifications/android')
|
||||
|
||||
include ':capacitor-share'
|
||||
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ const config: CapacitorConfig = {
|
||||
]
|
||||
}
|
||||
},
|
||||
PushNotifications: {
|
||||
presentationOptions: ['badge', 'sound', 'alert']
|
||||
},
|
||||
SplashScreen: {
|
||||
launchShowDuration: 3000,
|
||||
launchAutoHide: true,
|
||||
|
||||
@@ -18,6 +18,7 @@ def capacitor_pods
|
||||
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
|
||||
pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
|
||||
pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
|
||||
pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
|
||||
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
|
||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||
pod 'CapawesomeCapacitorFilePicker', :path => '../../node_modules/@capawesome/capacitor-file-picker'
|
||||
|
||||
@@ -17,6 +17,8 @@ PODS:
|
||||
- CapacitorMlkitBarcodeScanning (6.2.0):
|
||||
- Capacitor
|
||||
- GoogleMLKit/BarcodeScanning (= 5.0.0)
|
||||
- CapacitorPushNotifications (6.0.5):
|
||||
- Capacitor
|
||||
- CapacitorShare (6.0.3):
|
||||
- Capacitor
|
||||
- CapacitorStatusBar (6.0.2):
|
||||
@@ -99,6 +101,7 @@ DEPENDENCIES:
|
||||
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
|
||||
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
|
||||
- "CapacitorPushNotifications (from `../../node_modules/@capacitor/push-notifications`)"
|
||||
- "CapacitorShare (from `../../node_modules/@capacitor/share`)"
|
||||
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
|
||||
- "CapawesomeCapacitorFilePicker (from `../../node_modules/@capawesome/capacitor-file-picker`)"
|
||||
@@ -138,6 +141,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/@capacitor/filesystem"
|
||||
CapacitorMlkitBarcodeScanning:
|
||||
:path: "../../node_modules/@capacitor-mlkit/barcode-scanning"
|
||||
CapacitorPushNotifications:
|
||||
:path: "../../node_modules/@capacitor/push-notifications"
|
||||
CapacitorShare:
|
||||
:path: "../../node_modules/@capacitor/share"
|
||||
CapacitorStatusBar:
|
||||
@@ -156,6 +161,7 @@ SPEC CHECKSUMS:
|
||||
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
|
||||
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
||||
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
|
||||
CapacitorPushNotifications: 35abece14371c57172e8321c9ccc8b6fa35fabfe
|
||||
CapacitorShare: d2a742baec21c8f3b92b361a2fbd2401cdd8288e
|
||||
CapacitorStatusBar: b16799a26320ffa52f6c8b01737d5a95bbb8f3eb
|
||||
CapawesomeCapacitorFilePicker: c40822f0a39f86855321943c7829d52bca7f01bd
|
||||
@@ -175,6 +181,6 @@ SPEC CHECKSUMS:
|
||||
TimesafariDailyNotificationPlugin: 95e0a6238f6586ca5190cc6d653ac882fb0e82ac
|
||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||
|
||||
PODFILE CHECKSUM: bf247ff01f83709ef1010f328f5fb4ab5370cb41
|
||||
PODFILE CHECKSUM: efd66cc2a3ebb7b5bae6a7c15e52bda1ab546cf6
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
4036
package-lock.json
generated
4036
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -148,6 +148,7 @@
|
||||
"@capacitor/core": "^6.2.0",
|
||||
"@capacitor/filesystem": "^6.0.0",
|
||||
"@capacitor/ios": "^6.2.0",
|
||||
"@capacitor/push-notifications": "^6.0.5",
|
||||
"@capacitor/share": "^6.0.3",
|
||||
"@capacitor/status-bar": "^6.0.2",
|
||||
"@capawesome/capacitor-file-picker": "^6.2.0",
|
||||
@@ -194,6 +195,7 @@
|
||||
"electron-builder": "^26.0.12",
|
||||
"ethereum-cryptography": "^2.1.3",
|
||||
"ethereumjs-util": "^7.1.5",
|
||||
"firebase": "^12.12.1",
|
||||
"jdenticon": "^3.2.0",
|
||||
"js-generate-password": "^0.1.9",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
||||
@@ -43,7 +43,10 @@ import "./utils/safeAreaInset";
|
||||
|
||||
// Load Daily Notification plugin at startup so native performRecovery() runs at launch (rollover recovery)
|
||||
import "@timesafari/daily-notification-plugin";
|
||||
import { configureNativeFetcherIfReady } from "@/services/notifications";
|
||||
import {
|
||||
configureNativeFetcherIfReady,
|
||||
initializeNativePushAndFirebaseMessaging,
|
||||
} from "@/services/notifications";
|
||||
|
||||
logger.log("[Capacitor] 🚀 Starting initialization");
|
||||
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
|
||||
@@ -474,6 +477,8 @@ setTimeout(async () => {
|
||||
);
|
||||
await registerDeepLinkListener();
|
||||
logger.info(`[Main] 🎉 Deep link system fully initialized!`);
|
||||
// Firebase Messaging (JS) + Capacitor PushNotifications (FCM/APNs token, delivery listeners)
|
||||
await initializeNativePushAndFirebaseMessaging();
|
||||
// Configure native fetcher for API-driven daily notifications (activeDid + JWT)
|
||||
await configureNativeFetcherIfReady();
|
||||
} catch (error) {
|
||||
|
||||
185
src/services/notifications/firebaseMessagingClient.ts
Normal file
185
src/services/notifications/firebaseMessagingClient.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Firebase Cloud Messaging (JS SDK) + Capacitor Push Notifications (native bridge).
|
||||
*
|
||||
* Initializes the Firebase web app when VITE_FIREBASE_* env vars are set, wires
|
||||
* Capacitor push listeners, requests permission before registration/token flow,
|
||||
* and attaches Firebase messaging when the browser/WebView reports support.
|
||||
*/
|
||||
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { PushNotifications } from "@capacitor/push-notifications";
|
||||
import {
|
||||
type FirebaseApp,
|
||||
type FirebaseOptions,
|
||||
getApps,
|
||||
initializeApp,
|
||||
} from "firebase/app";
|
||||
import {
|
||||
getMessaging,
|
||||
getToken,
|
||||
isSupported,
|
||||
onMessage,
|
||||
} from "firebase/messaging";
|
||||
import { logger } from "@/utils/logger";
|
||||
|
||||
const LOG = "[FirebaseMessaging]";
|
||||
|
||||
let firebaseAppSingleton: FirebaseApp | null = null;
|
||||
let nativeInitPromise: Promise<void> | null = null;
|
||||
|
||||
function readFirebaseOptions(): FirebaseOptions | null {
|
||||
const env = import.meta.env;
|
||||
const apiKey = env.VITE_FIREBASE_API_KEY as string | undefined;
|
||||
const projectId = env.VITE_FIREBASE_PROJECT_ID as string | undefined;
|
||||
const appId = env.VITE_FIREBASE_APP_ID as string | undefined;
|
||||
const messagingSenderId = env.VITE_FIREBASE_MESSAGING_SENDER_ID as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
if (!apiKey || !projectId || !appId || !messagingSenderId) {
|
||||
logger.debug(
|
||||
`${LOG} Missing one or more VITE_FIREBASE_* keys; Firebase app not initialized`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const authDomain =
|
||||
(env.VITE_FIREBASE_AUTH_DOMAIN as string | undefined) ||
|
||||
`${projectId}.firebaseapp.com`;
|
||||
const storageBucket =
|
||||
(env.VITE_FIREBASE_STORAGE_BUCKET as string | undefined) ||
|
||||
`${projectId}.appspot.com`;
|
||||
|
||||
const opts: FirebaseOptions = {
|
||||
apiKey,
|
||||
authDomain,
|
||||
projectId,
|
||||
storageBucket,
|
||||
messagingSenderId,
|
||||
appId,
|
||||
};
|
||||
|
||||
const measurementId = env.VITE_FIREBASE_MEASUREMENT_ID as string | undefined;
|
||||
if (measurementId) {
|
||||
opts.measurementId = measurementId;
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a single Firebase app instance for the client when config is present.
|
||||
*/
|
||||
export function ensureFirebaseApp(): FirebaseApp | null {
|
||||
if (firebaseAppSingleton) {
|
||||
return firebaseAppSingleton;
|
||||
}
|
||||
const options = readFirebaseOptions();
|
||||
if (!options) {
|
||||
return null;
|
||||
}
|
||||
firebaseAppSingleton =
|
||||
getApps().length > 0 ? getApps()[0]! : initializeApp(options);
|
||||
logger.info(`${LOG} Firebase app initialized`);
|
||||
return firebaseAppSingleton;
|
||||
}
|
||||
|
||||
async function attachFirebaseMessagingIfSupported(
|
||||
app: FirebaseApp,
|
||||
): Promise<void> {
|
||||
if (!(await isSupported())) {
|
||||
logger.debug(
|
||||
`${LOG} firebase/messaging not supported in this context; skipping getMessaging`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const messaging = getMessaging(app);
|
||||
const vapidKey = import.meta.env.VITE_FIREBASE_VAPID_KEY as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
try {
|
||||
const token = await getToken(
|
||||
messaging,
|
||||
vapidKey ? { vapidKey } : undefined,
|
||||
);
|
||||
logger.info(`${LOG} Firebase getToken completed`, {
|
||||
tokenPrefix: token ? `${token.slice(0, 12)}…` : "(empty)",
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`${LOG} Firebase getToken failed (common on native WebView without SW)`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
|
||||
onMessage(messaging, (payload) => {
|
||||
logger.debug(`${LOG} onMessage (foreground)`, payload);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Native: register Capacitor push listeners, request permissions, register for push,
|
||||
* then initialize Firebase Messaging when env config and platform support allow.
|
||||
*/
|
||||
async function initializeNativePushAndFirebaseMessagingImpl(): Promise<void> {
|
||||
if (!Capacitor.isNativePlatform()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const app = ensureFirebaseApp();
|
||||
|
||||
await PushNotifications.addListener("registration", (token) => {
|
||||
logger.info(`${LOG} Capacitor registration token`, {
|
||||
valuePrefix: token.value ? `${token.value.slice(0, 12)}…` : "(empty)",
|
||||
});
|
||||
});
|
||||
|
||||
await PushNotifications.addListener("registrationError", (err) => {
|
||||
logger.error(`${LOG} registrationError`, err);
|
||||
});
|
||||
|
||||
await PushNotifications.addListener(
|
||||
"pushNotificationReceived",
|
||||
(notification) => {
|
||||
logger.debug(`${LOG} pushNotificationReceived`, notification);
|
||||
},
|
||||
);
|
||||
|
||||
await PushNotifications.addListener(
|
||||
"pushNotificationActionPerformed",
|
||||
(action) => {
|
||||
logger.debug(`${LOG} pushNotificationActionPerformed`, action);
|
||||
},
|
||||
);
|
||||
|
||||
const perm = await PushNotifications.requestPermissions();
|
||||
if (perm.receive !== "granted") {
|
||||
logger.warn(`${LOG} Push permission not granted`, perm);
|
||||
return;
|
||||
}
|
||||
|
||||
await PushNotifications.register();
|
||||
|
||||
if (app) {
|
||||
await attachFirebaseMessagingIfSupported(app);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`${LOG} Native push / Firebase messaging init failed`, err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Idempotent startup hook for Capacitor iOS/Android.
|
||||
*/
|
||||
export function initializeNativePushAndFirebaseMessaging(): Promise<void> {
|
||||
if (!Capacitor.isNativePlatform()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!nativeInitPromise) {
|
||||
nativeInitPromise = initializeNativePushAndFirebaseMessagingImpl();
|
||||
}
|
||||
return nativeInitPromise;
|
||||
}
|
||||
@@ -18,6 +18,10 @@ export { NativeNotificationService } from "./NativeNotificationService";
|
||||
export { WebPushNotificationService } from "./WebPushNotificationService";
|
||||
|
||||
export { configureNativeFetcherIfReady } from "./nativeFetcherConfig";
|
||||
export {
|
||||
ensureFirebaseApp,
|
||||
initializeNativePushAndFirebaseMessaging,
|
||||
} from "./firebaseMessagingClient";
|
||||
export { syncStarredPlansToNativePlugin } from "./syncStarredPlansToNativePlugin";
|
||||
export {
|
||||
buildDualScheduleConfig,
|
||||
|
||||
Reference in New Issue
Block a user