Skip to content
This repository was archived by the owner on Mar 15, 2023. It is now read-only.

Commit 067eeef

Browse files
committed
Add os/browser info in subscription
Get rid of BroadcastChannel (not supported on Safari) Report viewed notification
1 parent 5db3ee5 commit 067eeef

File tree

10 files changed

+132
-95
lines changed

10 files changed

+132
-95
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"require": {
1919
"php": ">=7.3",
2020
"yiisoft/yii2": "~2.0.13",
21-
"minishlink/web-push": "^6.0"
21+
"minishlink/web-push": "^6.0",
22+
"matomo/device-detector": "^4.3"
2223
},
2324
"require-dev": {
2425
"codeception/codeception": "^4.1",

src/assets/sw.js

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1-
const wpnBroadcast = new BroadcastChannel("yii2-web-push-notifications");
1+
const reportAction = (payload) => {
2+
console.log("Broadcasting message", payload)
3+
fetch(`/web-push/report`, {
4+
method: 'POST',
5+
body: JSON.stringify(payload)
6+
}).then(() => {
7+
console.log("Action reported");
8+
}
9+
).catch(error => {
10+
console.log("Action reporting failed", error);
11+
});
12+
}
213

3-
self.addEventListener("push", function(event) {
14+
self.addEventListener("push", function (event) {
415
console.log("in push event listener", event);
516
if (!(self.Notification && self.Notification.permission === "granted")) {
617
console.error("No notification ?");
@@ -11,51 +22,57 @@ self.addEventListener("push", function(event) {
1122
console.log("Got payload", payload);
1223
const decoded = JSON.parse(payload);
1324

14-
const { title, ...options } = decoded;
25+
const {title, ...options} = decoded;
1526

16-
return self.registration.showNotification(title, options);
27+
return self.registration.showNotification(title, options).then(() => {
28+
reportAction( {
29+
action: "view",
30+
endpoint: event.notification.data.endpoint,
31+
campaignId: event.notification.data.campaignId
32+
});
33+
});
1734
};
1835

1936
if (event.data) {
2037
const message = event.data.text();
2138
event.waitUntil(sendNotification(message));
39+
2240
}
2341
});
2442

25-
self.addEventListener("notificationclick", function(event) {
43+
self.addEventListener("notificationclick", function (event) {
2644
event.notification.close();
2745
// This looks to see if the current window is already open and focuses if it is
2846
event.waitUntil(
29-
clients
30-
.matchAll({
31-
type: "window"
32-
})
33-
.then(function() {
34-
if (clients.openWindow) {
35-
const data = event.notification.data; // the payload from before
36-
return clients.openWindow(data.url); // open it
37-
}
38-
})
39-
.then(() => {
40-
wpnBroadcast.postMessage({
41-
action: "click",
42-
campaignId: event.notification.data.campaignId
43-
});
44-
})
47+
clients.matchAll({
48+
type: "window"
49+
}).then(function () {
50+
if (clients.openWindow) {
51+
const data = event.notification.data; // the payload from before
52+
return clients.openWindow(data.url); // open it
53+
}
54+
}).then(() => {
55+
reportAction( {
56+
action: "click",
57+
endpoint: event.notification.data.endpoint,
58+
campaignId: event.notification.data.campaignId
59+
});
60+
})
4561
);
4662
});
4763

48-
self.addEventListener("install", function(e) {
64+
self.addEventListener("install", function (e) {
4965
e.waitUntil(self.skipWaiting());
5066
});
5167

52-
self.addEventListener("notificationclose", function(e) {
53-
wpnBroadcast.postMessage({
68+
self.addEventListener("notificationclose", function (e) {
69+
reportAction( {
5470
action: "dismiss",
55-
campaignId: e.notification.data.campaignId
71+
campaignId: e.notification.data.campaignId,
72+
endpoint: e.notification.data.endpoint,
5673
});
5774
});
5875

59-
self.addEventListener("activate", function(e) {
76+
self.addEventListener("activate", function (e) {
6077
e.waitUntil(self.clients.claim());
6178
});

src/assets/web-push.js

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,24 @@ var SubscriptionStatus;
1717
SubscriptionStatus["BLOCKED"] = "blocked";
1818
})(SubscriptionStatus || (SubscriptionStatus = {}));
1919
var WebPush = /** @class */ (function () {
20-
function WebPush(appId, publicKey, controller, localStorageKey) {
21-
var _this = this;
20+
function WebPush(appId, publicKey, controller, syncInterval) {
2221
if (controller === void 0) { controller = "/wpn/default"; }
23-
if (localStorageKey === void 0) { localStorageKey = "yii2-wpn-endpoint"; }
22+
if (syncInterval === void 0) { syncInterval = 1000 * 60 * 10; }
23+
this.LS = {
24+
ENDPOINT: "yii2-wpn-endpoint",
25+
LAST_SYNC: "yii2-wpn-last_sync"
26+
};
2427
this.appId = appId;
2528
this.publicKey = publicKey;
2629
this.controller = controller;
27-
this.localStorageKey = localStorageKey;
28-
var wpnBroadcast = new BroadcastChannel("yii2-web-push-notifications");
29-
wpnBroadcast.onmessage = function (event) {
30-
var _a = event.data, action = _a.action, campaignId = _a.campaignId;
31-
$.post(_this.controller + "/report?appId=" + _this.appId, __assign({ endpoint: localStorage.getItem(_this.localStorageKey), action: action,
32-
campaignId: campaignId }, _this.getCsrfParams()), function () {
33-
_this.log("Action reported", action);
34-
}).fail(function (error) {
35-
_this.log("Action reporting failed", action, error);
36-
});
37-
};
30+
this.syncInterval = syncInterval;
3831
}
39-
WebPush.prototype.setupRegistration = function (swPath) {
32+
WebPush.prototype.setupRegistration = function (swPath, successCb, failureCb, shouldMigrate) {
4033
var _this = this;
4134
if (swPath === void 0) { swPath = "/sw.js"; }
35+
if (successCb === void 0) { successCb = function (status) { }; }
36+
if (failureCb === void 0) { failureCb = function (error) { }; }
37+
if (shouldMigrate === void 0) { shouldMigrate = function (context) { return false; }; }
4238
if (!("serviceWorker" in navigator)) {
4339
this.log("Service workers are not supported by this browser");
4440
return false;
@@ -55,11 +51,16 @@ var WebPush = /** @class */ (function () {
5551
this.log("Notifications are denied by the user");
5652
return false;
5753
}
58-
navigator.serviceWorker.register(swPath).then(function (registration) {
59-
_this.log("ServiceWorker registration success: ", registration);
60-
_this.checkSubscription();
54+
navigator.serviceWorker.register(swPath).then(function () {
55+
var lastSync = parseInt(localStorage.getItem(_this.LS.LAST_SYNC));
56+
var now = Date.now();
57+
if (!lastSync || (now - lastSync > _this.syncInterval)) {
58+
console.log(now, lastSync, now - lastSync, _this.syncInterval);
59+
_this.checkSubscription(successCb, failureCb, shouldMigrate);
60+
localStorage.setItem(_this.LS.LAST_SYNC, String(now));
61+
}
6162
}, function (err) {
62-
this.log("ServiceWorker registration failed: ", err);
63+
_this.log("ServiceWorker registration failed: ", err);
6364
});
6465
};
6566
WebPush.prototype.checkSubscription = function (successCb, failureCb, shouldMigrate) {
@@ -115,7 +116,7 @@ var WebPush = /** @class */ (function () {
115116
.then(function (subscription) {
116117
// Subscription was successful
117118
// create subscription on your server
118-
localStorage.setItem(_this.localStorageKey, subscription.endpoint);
119+
localStorage.setItem(_this.LS.ENDPOINT, subscription.endpoint);
119120
return _this.sync(subscription, "POST");
120121
})
121122
.then(function (subscription) { return success(subscription); }) // update your UI
@@ -157,7 +158,7 @@ var WebPush = /** @class */ (function () {
157158
})
158159
.then(function (subscription) { return subscription.unsubscribe(); })
159160
.then(function () {
160-
localStorage.removeItem(_this.localStorageKey);
161+
localStorage.removeItem(_this.LS.ENDPOINT);
161162
success();
162163
})["catch"](function (e) {
163164
// We failed to unsubscribe, this can lead to
@@ -173,7 +174,7 @@ var WebPush = /** @class */ (function () {
173174
var contentEncoding = (PushManager.supportedContentEncodings || [
174175
"aesgcm"
175176
])[0];
176-
localStorage.setItem(this.localStorageKey, subscription.endpoint);
177+
localStorage.setItem(this.LS.ENDPOINT, subscription.endpoint);
177178
return new Promise(function (resolve, reject) {
178179
$.ajax({
179180
url: _this.controller + "/sync?appId=" + _this.appId,

src/assets/web-push.ts

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,41 +9,26 @@ class WebPush {
99
readonly appId;
1010
readonly publicKey;
1111
readonly controller;
12-
readonly localStorageKey;
12+
13+
private readonly LS = {
14+
ENDPOINT: "yii2-wpn-endpoint",
15+
LAST_SYNC: "yii2-wpn-last_sync",
16+
};
17+
private readonly syncInterval: number;
1318

1419
constructor(
1520
appId: number,
1621
publicKey: string,
1722
controller = "/wpn/default",
18-
localStorageKey = "yii2-wpn-endpoint"
23+
syncInterval = 1000 * 60 * 10
1924
) {
2025
this.appId = appId;
2126
this.publicKey = publicKey;
2227
this.controller = controller;
23-
this.localStorageKey = localStorageKey;
24-
25-
const wpnBroadcast = new BroadcastChannel("yii2-web-push-notifications");
26-
wpnBroadcast.onmessage = event => {
27-
const { action, campaignId } = event.data;
28-
29-
$.post(
30-
`${this.controller}/report?appId=${this.appId}`,
31-
{
32-
endpoint: localStorage.getItem(this.localStorageKey),
33-
action,
34-
campaignId,
35-
...this.getCsrfParams()
36-
},
37-
() => {
38-
this.log("Action reported", action);
39-
}
40-
).fail(error => {
41-
this.log("Action reporting failed", action, error);
42-
});
43-
};
28+
this.syncInterval = syncInterval;
4429
}
4530

46-
setupRegistration(swPath = "/sw.js") {
31+
setupRegistration(swPath = "/sw.js", successCb = (status: SubscriptionStatus) => {}, failureCb = (error) => {}, shouldMigrate = (context) => false) {
4732
if (!("serviceWorker" in navigator)) {
4833
this.log("Service workers are not supported by this browser");
4934
return false;
@@ -59,11 +44,17 @@ class WebPush {
5944
}
6045

6146
navigator.serviceWorker.register(swPath).then(
62-
registration => {
63-
this.log("ServiceWorker registration success: ", registration);
64-
this.checkSubscription();
47+
() => {
48+
const lastSync = parseInt(localStorage.getItem(this.LS.LAST_SYNC));
49+
const now = Date.now();
50+
51+
if (!lastSync || (now - lastSync > this.syncInterval)) {
52+
console.log(now, lastSync, now - lastSync, this.syncInterval)
53+
this.checkSubscription(successCb, failureCb, shouldMigrate);
54+
localStorage.setItem(this.LS.LAST_SYNC, String(now));
55+
}
6556
},
66-
function(err) {
57+
(err) => {
6758
this.log("ServiceWorker registration failed: ", err);
6859
}
6960
);
@@ -120,7 +111,7 @@ class WebPush {
120111
.then((subscription: PushSubscription) => {
121112
// Subscription was successful
122113
// create subscription on your server
123-
localStorage.setItem(this.localStorageKey, subscription.endpoint);
114+
localStorage.setItem(this.LS.ENDPOINT, subscription.endpoint);
124115
return this.sync(subscription, "POST");
125116
})
126117
.then(
@@ -162,7 +153,7 @@ class WebPush {
162153
})
163154
.then(subscription => subscription.unsubscribe())
164155
.then(() => {
165-
localStorage.removeItem(this.localStorageKey);
156+
localStorage.removeItem(this.LS.ENDPOINT);
166157
success();
167158
})
168159
.catch(e => {
@@ -180,7 +171,7 @@ class WebPush {
180171
"aesgcm"
181172
])[0];
182173

183-
localStorage.setItem(this.localStorageKey, subscription.endpoint);
174+
localStorage.setItem(this.LS.ENDPOINT, subscription.endpoint);
184175
return new Promise((resolve, reject) => {
185176
$.ajax({
186177
url: `${this.controller}/sync?appId=${this.appId}`,

src/components/Pusher.php

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,25 @@ public function sendPush(WpnCampaign $campaign)
4343
],
4444
];
4545

46-
$payload = Json::encode($campaign->options);
4746

48-
/** @var WpnSubscription[] $subscriptions */
49-
$subscriptions = ArrayHelper::index(
50-
WpnSubscription::find()
47+
48+
$query = WpnSubscription::find()
5149
->where(['subscribed' => true, 'app_id' => $campaign->app_id])
52-
->orderBy(['last_seen' => SORT_DESC])
53-
->all(),
54-
'endpoint'
55-
);
50+
->orderBy(['last_seen' => SORT_DESC]);
51+
52+
if ($campaign->test_only) {
53+
$query->andWhere(['test_user' => 1]);
54+
}
55+
56+
/** @var WpnSubscription[] $subscriptions */
57+
$subscriptions = ArrayHelper::index($query->all(), 'endpoint');
5658

5759
foreach ($subscriptions as $subscription) {
5860
try {
59-
$this->wp->queueNotification($subscription, $payload, [], $auth);
61+
$options = $campaign->options;
62+
$options['data']['endpoint'] = $subscription->endpoint;
63+
64+
$this->wp->queueNotification($subscription, Json::encode($options), [], $auth);
6065
} catch (\Exception $e) {
6166
$subscription->last_error = $e->getMessage();
6267
$subscription->save();

src/controllers/DefaultController.php

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace machour\yii2\wpn\controllers;
44

5+
use DeviceDetector\DeviceDetector;
56
use machour\yii2\wpn\components\Pusher;
67
use machour\yii2\wpn\exceptions\SubscriptionNotFound;
78
use machour\yii2\wpn\models\WpnCampaign;
@@ -19,6 +20,18 @@
1920

2021
class DefaultController extends Controller
2122
{
23+
/**
24+
* @inheritdoc
25+
*/
26+
public function beforeAction($action): bool
27+
{
28+
if ($action->id == 'report') {
29+
$this->enableCsrfValidation = false;
30+
}
31+
32+
return parent::beforeAction($action);
33+
}
34+
2235
public function behaviors(): array
2336
{
2437
return [
@@ -51,6 +64,7 @@ public function actionSync($appId): \yii\web\Response
5164

5265
switch ($request->method) {
5366
case 'POST':
67+
5468
$subscription = new WpnSubscription([
5569
'app_id' => $appId,
5670
'endpoint' => $data['endpoint'],
@@ -65,6 +79,11 @@ public function actionSync($appId): \yii\web\Response
6579
'last_seen' => new Expression('NOW()'),
6680
]);
6781

82+
$dd = new DeviceDetector($subscription->ua);
83+
$dd->parse();
84+
$subscription->os = $dd->getOs('name');
85+
$subscription->browser = $dd->getClient('name');
86+
6887
if ($subscription->save()) {
6988
return $this->asJson(['success' => true, 'user_id' => $subscription->id]);
7089
} else {
@@ -113,10 +132,11 @@ public function actionSync($appId): \yii\web\Response
113132
public function actionReport(): \yii\web\Response
114133
{
115134
$request = Yii::$app->request;
135+
$data = Json::decode($request->rawBody);
116136

117-
$campaign_id = $request->post('campaignId', false);
118-
$endpoint = $request->post('endpoint', false);
119-
$action = $request->post('action', false);
137+
$campaign_id = $data['campaignId'];
138+
$endpoint = $data['endpoint'];
139+
$action = $data['action'];
120140

121141
$campaign = WpnCampaign::findOne($campaign_id);
122142
if (!$campaign) {

0 commit comments

Comments
 (0)