Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 2 additions & 21 deletions Events.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 .= <<<JS
// Give the service worker access to Firebase Messaging.
importScripts('{$baseUrl}/firebase-app-compat.js');
importScripts('{$baseUrl}/firebase-messaging-compat.js');

firebase.initializeApp({
messagingSenderId: "{$pushDriver->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)
Expand Down
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion module.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"humhub": {
"minVersion": "1.18"
},
"version": "2.2.7",
"version": "2.2.8",
"screenshots": [
"resources/screenshot1.PNG",
"resources/screenshot2.PNG"
Expand Down
50 changes: 50 additions & 0 deletions resources/js/humhub.firebase.worker.js
Original file line number Diff line number Diff line change
@@ -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); });
})
);
});
48 changes: 48 additions & 0 deletions services/ServiceWorkerService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace humhub\modules\fcmPush\services;

use humhub\modules\fcmPush\assets\FirebaseAsset;
use humhub\modules\fcmPush\Module;
use Yii;

/**
* Builds the JavaScript appended to the PWA service worker
* (see Events::onServiceWorkerControllerInit()).
*/
class ServiceWorkerService
{
public function __construct(private Module $module)
{
}

public function getJs(): string
{
$bundle = FirebaseAsset::register(Yii::$app->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 .= <<<JS
importScripts('{$baseUrl}/firebase-app-compat.js');
importScripts('{$baseUrl}/firebase-messaging-compat.js');

firebase.initializeApp({
messagingSenderId: "{$pushDriver->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;
}
}
Loading