diff --git a/Events.php b/Events.php index 47d64e4..2c7b6fc 100644 --- a/Events.php +++ b/Events.php @@ -7,7 +7,7 @@ use humhub\modules\fcmPush\components\NotificationTargetProvider; use humhub\modules\fcmPush\helpers\MobileAppHelper; use humhub\modules\fcmPush\helpers\WebAppHelper; -use humhub\modules\fcmPush\services\DriverService; +use humhub\modules\fcmPush\services\ServiceWorkerService; use humhub\modules\fcmPush\widgets\RegisterDeviceTokenButton; use humhub\modules\notification\targets\MobileTargetProvider; use humhub\modules\notification\widgets\NotificationSettingsForm; @@ -43,27 +43,8 @@ public static function onServiceWorkerControllerInit($event): void return; } - $bundle = FirebaseAsset::register(Yii::$app->view); - - $pushDriver = (new DriverService($module->getConfigureForm()))->getWebDriver(); - $baseUrl = Yii::getAlias($bundle->baseUrl); - // Service Worker Addons - $controller->additionalJs .= <<getSenderId()}", - projectId: "{$module->getConfigureForm()->getJsonParam('project_id')}", - appId: "{$module->getConfigureForm()->firebaseAppId}", - apiKey: "{$module->getConfigureForm()->firebaseApiKey}", - }); - - // Initialize Firebase Cloud Messaging and get a reference to the service - firebase.messaging(); -JS; + $controller->additionalJs .= (new ServiceWorkerService($module))->getJs(); } public static function onLayoutAddonInit($event) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 459e495..9736d44 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +2.2.8 (Unreleased) +------------------ +- Fix #100: Open the notification URL when a notification is clicked while the app is already open — Firebase only focuses the window without navigating (iOS PWAs with the app already running are not covered: WebKit never dispatches the click event there) + 2.2.7 (July 9, 2026) -------------------- - Fix #96: Notification.requestPermission() not bound to user gesture — iOS PWA push never shown diff --git a/module.json b/module.json index abc0975..5165053 100644 --- a/module.json +++ b/module.json @@ -11,7 +11,7 @@ "humhub": { "minVersion": "1.18" }, - "version": "2.2.7", + "version": "2.2.8", "screenshots": [ "resources/screenshot1.PNG", "resources/screenshot2.PNG" diff --git a/resources/js/humhub.firebase.worker.js b/resources/js/humhub.firebase.worker.js new file mode 100644 index 0000000..03bfb98 --- /dev/null +++ b/resources/js/humhub.firebase.worker.js @@ -0,0 +1,50 @@ +/** + * Service worker addon for FCM push notifications, appended to the PWA service + * worker by ServiceWorkerService before the Firebase importScripts, so the + * notificationclick handler below is called before Firebase's own handler. + */ + +// Activate updated service workers immediately instead of waiting for all +// app windows to close - iOS PWAs may otherwise keep an old SW for a long time. +self.addEventListener('install', function () { + self.skipWaiting(); +}); +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()); +}); + +// Handle notification clicks ourselves, before Firebase's own handler, which +// only focuses an already-open window without navigating it (it delegates +// navigation to a postMessage the page never listens for). +// +// Known limitation (iOS/WebKit bug, iOS 17/18): when the PWA is already +// running, iOS never dispatches notificationclick at all, so tapping a +// notification only brings the app to the foreground without navigating. +// This is intentionally not worked around here - no service-worker code can +// ever see that tap. See https://github.com/humhub/fcm-push/pull/99 for an +// exploration of a page-side heuristic covering that case. +self.addEventListener('notificationclick', function (event) { + const fcmMsg = event.notification && event.notification.data && event.notification.data.FCM_MSG; + const url = fcmMsg && ((fcmMsg.fcmOptions && fcmMsg.fcmOptions.link) || (fcmMsg.data && fcmMsg.data.url)); + if (!url) { + return; // Not one of our notifications - leave it to Firebase's handler + } + + // Take over completely: prevent Firebase's handler from running. + event.stopImmediatePropagation(); + event.notification.close(); + + event.waitUntil( + self.clients.matchAll({type: 'window', includeUncontrolled: true}).then(function (clientList) { + // matchAll() returns window clients most-recently-focused first. + const client = clientList.find(function (c) { return 'navigate' in c; }); + if (!client) { + return self.clients.openWindow(url); + } + return Promise.resolve(client.focus()) + .catch(function () { return client; }) + .then(function (c) { return (c || client).navigate(url); }) + .catch(function () { return self.clients.openWindow(url); }); + }) + ); +}); diff --git a/services/ServiceWorkerService.php b/services/ServiceWorkerService.php new file mode 100644 index 0000000..0bb9584 --- /dev/null +++ b/services/ServiceWorkerService.php @@ -0,0 +1,48 @@ +view); + $baseUrl = Yii::getAlias($bundle->baseUrl); + + $configureForm = $this->module->getConfigureForm(); + $pushDriver = (new DriverService($configureForm))->getWebDriver(); + + // Notification click handling (must be registered before the Firebase importScripts) + $js = file_get_contents(dirname(__DIR__) . '/resources/js/humhub.firebase.worker.js'); + + // Give the service worker access to Firebase Messaging. + $js .= <<getSenderId()}", + projectId: "{$configureForm->getJsonParam('project_id')}", + appId: "{$configureForm->firebaseAppId}", + apiKey: "{$configureForm->firebaseApiKey}", + }); + + // Initialize Firebase Cloud Messaging and get a reference to the service + firebase.messaging(); +JS; + + return $js; + } +}