Also stuck with it for a long time, but managed to find a solution.
Here is the flow:
- Create firebase-messaging-sw.js and make sure that it will be available in the folder with your site assests after the deploy (in my case it is larevel and public/build/assets folder). Here is my file, just in case (pretty standard)
// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here. Other Firebase libraries
// are not available in the service worker.
// Replace 10.13.2 with latest version of the Firebase JS SDK.
importScripts('https://www.gstatic.com/firebasejs/10.13.2/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/10.13.2/firebase-messaging-compat.js');
// Initialize the Firebase app in the service worker by passing in
// your app's Firebase config object.
// https://firebase.google.com/docs/web/setup#config-object
firebase.initializeApp({
apiKey: "",
authDomain: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: "",
measurementId: "",
});
// Handle notification click event
self.addEventListener('notificationclick', (event) => {
// Close the notification
event.notification.close();
// Get the URL from notification data
const urlToOpen = event.notification.data?.url;
event.waitUntil(
clients.matchAll({type: 'window'}).then((windowClients) => {
// If no matching tab is found, open a new one
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
// Retrieve an instance of Firebase Messaging so that it can handle background
const messaging = firebase.messaging();
messaging.onBackgroundMessage((payload) => {
// Extract all data you want to preserve
const notificationData = {
url: payload.data?.url,
// Include any other data you need
...payload.data
};
// Customize notification here
const notificationTitle = payload.data?.title || 'Notification';
const notificationOptions = {
body: payload.data?.body,
icon: payload.data?.icon,
data: notificationData
};
self.registration.showNotification(notificationTitle, notificationOptions);
});
-
Create app proxy web point to return firebase-messaging-sw.js file. The url should look like https://my-shopify-store.myshopify.com/your-proxy-app/your-endpoint-for-firebase-messageing-sw.
Here is my example
public function getServiceWorker(Request $request)
{
$remoteUrl = match(config('app_stage')) {
'staging' => 'https://your-assets-url/build/assets/firebase-messaging-sw.js',
'production' => 'https://your-assets-url/build/assets/firebase-messaging-sw.js'
};
$response = Http::get($remoteUrl);
if ($response->successful()) {
return response($response->body(), 200)
->header('Content-Type', 'application/javascript');
}
abort(404, 'Remote script not found');
}
-
Add firebase.js file with client logic and import it to your frontend code (as any other js file), here is my example.
import { initializeApp } from "firebase/app";
import { getMessaging, onMessage, getToken } from "firebase/messaging";
import { detectPlatform } from './utils/common';
function getEnv(url) {
const match = url.match(/url-match/);
if (!match) return null;
return match[1] === 'smth' ? 'production' : 'staging';
}
const PROXY_URL = "...";
// Unregister any existing service workers
async function unregisterServiceWorkers() {
const registrations = await navigator.serviceWorker.getRegistrations();
console.log(registrations);
for (let registration of registrations) {
await registration.unregister();
console.log('Unregistered old service worker:', registration.scope);
}
}
// Client-side code (NOT in service worker)
async function registerServiceWorker() {
try {
const registration = await navigator.serviceWorker.register(`${PROXY_URL}get-service-worker?v=${Date.now()}`, {
updateViaCache: 'none', // Bypass HTTP cache
scope: PROXY_URL // Explicit scope
});
// WAIT until service worker is actually active
if (registration.installing) {
await new Promise(resolve => {
registration.installing.addEventListener('statechange', (e) => {
if (e.target.state === 'activated') {
resolve();
}
});
});
}
return registration;
} catch (error) {
console.error('SW registration failed:', error);
throw error;
}
}
const firebaseConfig = {
apiKey: "",
authDomain: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: "",
measurementId: "",
};
if (import.meta.env.PROD && 'serviceWorker' in navigator) {
const app = initializeApp(firebaseConfig);
const messaging = getMessaging(app);
window.safariWebPushRegistered = false;
const platform = detectPlatform();
const registerSW = () => registerServiceWorker()
.then(async (registration) => {
console.log('Service Worker registered with scope:', registration.scope);
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
throw new Error('Permission not granted for Notification');
}
const token = await getToken(messaging, {
vapidKey: '',
serviceWorkerRegistration: registration, // ✅ must pass this manually
});
window.fcmToken= token;
// Save token to server if needed, ajax to your update-fcm-token endpoint
// console.log(response);
window.safariWebPushRegistered= true;
})
.catch((err) => {
console.error('Error during service worker registration or token retrieval:', err);
});
// For MAC ISO it's only possible to register on user interaction (at least I didn't find other way)
if (platform.isIOS && platform.isDesktop) {
document.addEventListener("mousedown", (event) => {
if (window.safariWebPushRegistered) return;
// alert('This is mousedown event on IOS DESKTOP');
// console.log('This is mousedown event on IOS DESKTOP');
registerSW();
});
} else {
// alert('This is run from NON ios device');
registerSW();
}
// Handle notification click event
addEventListener('notificationclick', (event) => {
// Close the notification
event.notification.close();
// Get the URL from notification data
const urlToOpen = event.notification.data?.url;
event.waitUntil(
clients.matchAll({type: 'window'}).then((windowClients) => {
// If no matching tab is found, open a new one
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
onMessage(messaging, (payload) => {
// console.log("Foreground message received: ", payload);
// Extract all data you want to preserve
const notificationData = {
url: payload.data?.url || `${window.location.origin + PROXY_URL}`,
// Include any other data you need
...payload.data
};
const notificationTitle = payload.data?.title || 'Notification';
const notificationOptions = {
body: payload.data?.body,
icon: payload.data?.icon,
data: notificationData
};
new Notification(notificationTitle, notificationOptions);
});
} else {
console.warn('Service workers are not supported in this browser');
}
That should do the trick (at least did it for me)