@pusher/push-notifications-web
Advanced tools
Comparing version 0.12.0 to 1.0.0
import { ErrolTestClient } from './errol-client'; | ||
import { launchServer, createChromeWebDriver } from './test-utils'; | ||
import { launchServer, createChromeWebDriver, NOTIFICATIONS_GRANTED } from './test-utils'; | ||
@@ -12,3 +12,3 @@ let killServer = null; | ||
}) | ||
.then(() => createChromeWebDriver()) | ||
.then(() => createChromeWebDriver(NOTIFICATIONS_GRANTED)) | ||
.then(driver => { | ||
@@ -54,7 +54,9 @@ chromeDriver = driver; | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId: '1b880590-6301-4bb5-b34f-45db1c5f5644', | ||
}) | ||
.then(beamsClient => beamsClient.start()) | ||
.then(beamsClient => asyncScriptReturnCallback(beamsClient.deviceId)) | ||
}); | ||
return beamsClient | ||
.start() | ||
.then(() => beamsClient.getDeviceId()) | ||
.then(deviceId => asyncScriptReturnCallback(deviceId)) | ||
.catch(e => asyncScriptReturnCallback(e.message)); | ||
@@ -88,6 +90,8 @@ }); | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId: '1b880590-6301-4bb5-b34f-45db1c5f5644', | ||
}) | ||
.then(beamsClient => beamsClient.setUserId('cucas', tokenProvider)) | ||
}); | ||
return beamsClient | ||
.start() | ||
.then(() => beamsClient.setUserId('cucas', tokenProvider)) | ||
.then(() => asyncScriptReturnCallback('')) | ||
@@ -117,7 +121,9 @@ .catch(e => asyncScriptReturnCallback(e.message)); | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId: '1b880590-6301-4bb5-b34f-45db1c5f5644', | ||
}) | ||
.then(beamsClient => beamsClient.start()) | ||
.then(beamsClient => asyncScriptReturnCallback(beamsClient.deviceId)) | ||
}); | ||
return beamsClient | ||
.start() | ||
.then(() => beamsClient.getDeviceId()) | ||
.then(deviceId => asyncScriptReturnCallback(deviceId)) | ||
.catch(e => asyncScriptReturnCallback(e.message)); | ||
@@ -138,8 +144,7 @@ }); | ||
let beamsClient; | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId: '1b880590-6301-4bb5-b34f-45db1c5f5644', | ||
}) | ||
.then(c => (beamsClient = c)) | ||
.then(() => beamsClient.setUserId('cucas', tokenProvider)) | ||
}); | ||
beamsClient | ||
.setUserId('cucas', tokenProvider) | ||
.then(() => beamsClient.setUserId('ronaldinho', tokenProvider)) | ||
@@ -169,8 +174,7 @@ .then(() => asyncScriptReturnCallback('')) | ||
let beamsClient; | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId: '1b880590-6301-4bb5-b34f-45db1c5f5644', | ||
}) | ||
.then(c => (beamsClient = c)) | ||
.then(() => beamsClient.setUserId('cucas', tokenProvider)) | ||
}); | ||
beamsClient | ||
.setUserId('cucas', tokenProvider) | ||
.then(() => asyncScriptReturnCallback('')) | ||
@@ -192,7 +196,9 @@ .catch(e => asyncScriptReturnCallback(e.message)); | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId: '1b880590-6301-4bb5-b34f-45db1c5f5644', | ||
}) | ||
.then(beamsClient => beamsClient.start()) | ||
.then(beamsClient => asyncScriptReturnCallback(beamsClient.deviceId)) | ||
}); | ||
beamsClient | ||
.start() | ||
.then(() => beamsClient.getDeviceId()) | ||
.then(deviceId => asyncScriptReturnCallback(deviceId)) | ||
.catch(e => asyncScriptReturnCallback(e.message)); | ||
@@ -213,8 +219,7 @@ }); | ||
let beamsClient; | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId: '1b880590-6301-4bb5-b34f-45db1c5f5644', | ||
}) | ||
.then(c => (beamsClient = c)) | ||
.then(() => beamsClient.setUserId('', tokenProvider)) | ||
}); | ||
beamsClient | ||
.setUserId('', tokenProvider) | ||
.then(() => asyncScriptReturnCallback('')) | ||
@@ -236,7 +241,9 @@ .catch(e => asyncScriptReturnCallback(e.message)); | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId: '1b880590-6301-4bb5-b34f-45db1c5f5644', | ||
}) | ||
.then(beamsClient => beamsClient.start()) | ||
.then(beamsClient => asyncScriptReturnCallback(beamsClient.deviceId)) | ||
}); | ||
beamsClient | ||
.start() | ||
.then(() => beamsClient.getDeviceId()) | ||
.then(deviceId => asyncScriptReturnCallback(deviceId)) | ||
.catch(e => asyncScriptReturnCallback(e.message)); | ||
@@ -257,8 +264,7 @@ }); | ||
let beamsClient; | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId: '1b880590-6301-4bb5-b34f-45db1c5f5644', | ||
}) | ||
.then(c => (beamsClient = c)) | ||
.then(() => beamsClient.setUserId(undefined, tokenProvider)) | ||
}); | ||
beamsClient | ||
.setUserId(undefined, tokenProvider) | ||
.then(() => asyncScriptReturnCallback('')) | ||
@@ -265,0 +271,0 @@ .catch(e => asyncScriptReturnCallback(e.message)); |
@@ -1,2 +0,2 @@ | ||
import { launchServer, createChromeWebDriver } from './test-utils'; | ||
import { launchServer, createChromeWebDriver, NOTIFICATIONS_GRANTED } from './test-utils'; | ||
@@ -11,3 +11,3 @@ let killServer = null; | ||
}) | ||
.then(() => createChromeWebDriver()) | ||
.then(() => createChromeWebDriver(NOTIFICATIONS_GRANTED)) | ||
.then(driver => { | ||
@@ -28,5 +28,7 @@ chromeDriver = driver; | ||
const instanceId = 'deadc0de-2ce6-46e3-ad9a-5c02d0ab119b'; | ||
return PusherPushNotifications.init({ instanceId }) | ||
.then(beamsClient => beamsClient.start()) | ||
.then(beamsClient => asyncScriptReturnCallback(beamsClient.deviceId)) | ||
const beamsClient = new PusherPushNotifications.Client({ instanceId }); | ||
beamsClient | ||
.start() | ||
.then(() => beamsClient.getDeviceId()) | ||
.then(deviceId => asyncScriptReturnCallback(deviceId)) | ||
.catch(e => asyncScriptReturnCallback(e.message)); | ||
@@ -48,5 +50,7 @@ }); | ||
const instanceId = 'deadc0de-2ce6-46e3-ad9a-5c02d0ab119b'; | ||
return PusherPushNotifications.init({ instanceId }) | ||
.then(beamsClient => beamsClient.start()) | ||
.then(beamsClient => asyncScriptReturnCallback(beamsClient.deviceId)) | ||
const beamsClient = new PusherPushNotifications.Client({ instanceId }); | ||
beamsClient | ||
.start() | ||
.then(() => beamsClient.getDeviceId()) | ||
.then(deviceId => asyncScriptReturnCallback(deviceId)) | ||
.catch(e => asyncScriptReturnCallback(e.message)); | ||
@@ -64,5 +68,7 @@ }); | ||
const instanceId = 'deadc0de-2ce6-46e3-ad9a-5c02d0ab119b'; | ||
return PusherPushNotifications.init({ instanceId }) | ||
.then(beamsClient => beamsClient.start()) | ||
.then(beamsClient => asyncScriptReturnCallback(beamsClient.deviceId)) | ||
const beamsClient = new PusherPushNotifications.Client({ instanceId }); | ||
beamsClient | ||
.start() | ||
.then(() => beamsClient.getDeviceId()) | ||
.then(deviceId => asyncScriptReturnCallback(deviceId)) | ||
.catch(e => asyncScriptReturnCallback(e.message)); | ||
@@ -108,7 +114,8 @@ }); | ||
const instanceId = 'deadc0de-2ce6-46e3-ad9a-5c02d0ab119b'; | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId, | ||
}) | ||
.then(beamsClient => beamsClient.start()) | ||
.then(beamsClient => asyncScriptReturnCallback('start succeeded')) | ||
}); | ||
beamsClient | ||
.start() | ||
.then(() => asyncScriptReturnCallback('start succeeded')) | ||
.catch(e => asyncScriptReturnCallback(e.message)); | ||
@@ -147,3 +154,3 @@ }); | ||
const instanceId = 'deadc0de-2ce6-46e3-ad9a-5c02d0ab119b'; | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
serviceWorkerRegistration: await window.navigator.serviceWorker.register( | ||
@@ -153,5 +160,7 @@ '/service-worker.js' | ||
instanceId, | ||
}) | ||
.then(beamsClient => beamsClient.start()) | ||
.then(beamsClient => asyncScriptReturnCallback(beamsClient.deviceId)) | ||
}); | ||
beamsClient | ||
.start() | ||
.then(() => beamsClient.getDeviceId()) | ||
.then(deviceId => asyncScriptReturnCallback(deviceId)) | ||
.catch(e => asyncScriptReturnCallback(e.message)); | ||
@@ -175,8 +184,9 @@ }); | ||
.register('/not-the-root/service-worker.js') | ||
.then(registration => | ||
PusherPushNotifications.init({ | ||
.then(registration => { | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId, | ||
serviceWorkerRegistration: registration, | ||
}) | ||
) | ||
}); | ||
return beamsClient.start(); | ||
}) | ||
.catch(e => asyncScriptReturnCallback(e.message)); | ||
@@ -183,0 +193,0 @@ }); |
import { ErrolTestClient } from './errol-client'; | ||
import { launchServer, createChromeWebDriver } from './test-utils'; | ||
import { launchServer, createChromeWebDriver, NOTIFICATIONS_GRANTED } from './test-utils'; | ||
@@ -12,3 +12,3 @@ let killServer = null; | ||
}) | ||
.then(() => createChromeWebDriver()) | ||
.then(() => createChromeWebDriver(NOTIFICATIONS_GRANTED)) | ||
.then(driver => { | ||
@@ -55,6 +55,7 @@ chromeDriver = driver; | ||
return PusherPushNotifications.init({ instanceId }) | ||
.then(c => (window.beamsClient = c)) | ||
.then(() => window.beamsClient.start()) | ||
.then(() => asyncScriptReturnCallback(window.beamsClient.deviceId)) | ||
window.beamsClient = new PusherPushNotifications.Client({ instanceId }); | ||
window.beamsClient | ||
.start() | ||
.then(() => window.beamsClient.getDeviceId()) | ||
.then(deviceId => asyncScriptReturnCallback(deviceId)) | ||
.catch(e => asyncScriptReturnCallback(e.message)); | ||
@@ -88,5 +89,6 @@ }); | ||
return PusherPushNotifications.init({ instanceId }) | ||
.then(c => (window.beamsClient = c)) | ||
.then(() => asyncScriptReturnCallback(window.beamsClient.deviceId)) | ||
const beamsClient = new PusherPushNotifications.Client({ instanceId }); | ||
beamsClient | ||
.getDeviceId() | ||
.then(deviceId => asyncScriptReturnCallback(deviceId)) | ||
.catch(e => asyncScriptReturnCallback(e.message)); | ||
@@ -120,4 +122,5 @@ }); | ||
return PusherPushNotifications.init({ instanceId }) | ||
.then(beamsClient => beamsClient.stop()) | ||
const beamsClient = new PusherPushNotifications.Client({ instanceId }); | ||
beamsClient | ||
.stop() | ||
.then(() => asyncScriptReturnCallback('')) | ||
@@ -146,6 +149,7 @@ .catch(e => asyncScriptReturnCallback(e.message)); | ||
return PusherPushNotifications.init({ instanceId }) | ||
.then(c => (window.beamsClient = c)) | ||
.then(() => window.beamsClient.start()) | ||
.then(() => asyncScriptReturnCallback(window.beamsClient.deviceId)) | ||
const beamsClient = new PusherPushNotifications.Client({ instanceId }); | ||
beamsClient | ||
.start() | ||
.then(() => beamsClient.getDeviceId()) | ||
.then(deviceId => asyncScriptReturnCallback(deviceId)) | ||
.catch(e => asyncScriptReturnCallback(e.message)); | ||
@@ -161,5 +165,9 @@ }); | ||
return window.beamsClient | ||
const instanceId = '1b880590-6301-4bb5-b34f-45db1c5f5644'; | ||
const beamsClient = new PusherPushNotifications.Client({ instanceId }); | ||
beamsClient | ||
.clearAllState() | ||
.then(() => asyncScriptReturnCallback(window.beamsClient.deviceId)) | ||
.then(() => beamsClient.getDeviceId()) | ||
.then(deviceId => asyncScriptReturnCallback(deviceId)) | ||
.catch(e => asyncScriptReturnCallback(e.message)); | ||
@@ -166,0 +174,0 @@ }); |
@@ -22,2 +22,6 @@ const { spawn } = require('child_process'); | ||
export const NOTIFICATIONS_DEFAULT = 0 | ||
export const NOTIFICATIONS_GRANTED = 1 | ||
export const NOTIFICATIONS_DENIED = 2 | ||
beforeAll(() => { | ||
@@ -92,5 +96,6 @@ jest.setTimeout(SCRIPT_TIMEOUT_MS); | ||
* @async | ||
* @param {number} notificationPermission - The notification permission for the test server | ||
* @return {ChromeDriver} - Selenium webdriver instance (Chrome) | ||
*/ | ||
export async function createChromeWebDriver() { | ||
export async function createChromeWebDriver(notificationPermission = NOTIFICATIONS_DEFAULT) { | ||
// This is tricky for a few reasons: | ||
@@ -124,3 +129,3 @@ // 1. Selenium cannot accept the Web Push permission, so we have to | ||
const prefsFileName = `${prefsDir}/Preferences`; | ||
const configFileObject = createTempBrowserPreferences(TEST_SERVER_URL); | ||
const configFileObject = createTempBrowserPreferences(TEST_SERVER_URL, notificationPermission); | ||
const configFileString = JSON.stringify(configFileObject); | ||
@@ -151,4 +156,12 @@ | ||
function createTempBrowserPreferences(testSiteURL) { | ||
const testSiteKey = `${testSiteURL},*`; | ||
function createTempBrowserPreferences(testSiteURL, notificationPermission) { | ||
let notifications = {} | ||
if (notificationPermission !== NOTIFICATIONS_DEFAULT) { | ||
const testSiteKey = `${testSiteURL},*`; | ||
notifications = { | ||
[testSiteKey]: { | ||
setting: notificationPermission, | ||
} | ||
} | ||
} | ||
return { | ||
@@ -158,7 +171,3 @@ profile: { | ||
exceptions: { | ||
notifications: { | ||
[testSiteKey]: { | ||
setting: 1, | ||
}, | ||
}, | ||
notifications | ||
}, | ||
@@ -165,0 +174,0 @@ }, |
@@ -20,2 +20,9 @@ declare module '@pusher/push-notifications-web' { | ||
enum RegistrationState { | ||
PERMISSION_GRANTED_REGISTERED_WITH_BEAMS = 'PERMISSION_GRANTED_REGISTERED_WITH_BEAMS', | ||
PERMISSION_GRANTED_NOT_REGISTERED_WITH_BEAMS = 'PERMISSION_GRANTED_NOT_REGISTERED_WITH_BEAMS', | ||
PERMISSION_PROMPT_REQUIRED = 'PERMISSION_PROMPT_REQUIRED', | ||
PERMISSION_DENIED = 'PERMISSION_DENIED', | ||
} | ||
class PushNotificationsInstance { | ||
@@ -38,2 +45,3 @@ instanceId: string; | ||
clearAllState(): Promise<undefined>; | ||
getRegistrationState(): Promise<RegistrationState>; | ||
} | ||
@@ -40,0 +48,0 @@ |
{ | ||
"name": "@pusher/push-notifications-web", | ||
"version": "0.12.0", | ||
"version": "1.0.0", | ||
"description": "", | ||
@@ -10,2 +10,3 @@ "main": "dist/push-notifications-esm.js", | ||
"build:cdn": "rollup -c ./rollup/cdn.js", | ||
"build:sw": "rollup -c ./rollup/service-worker.js", | ||
"format": "prettier ./src/**/*.js --write", | ||
@@ -12,0 +13,0 @@ "lint": "eslint ./src/**/*.js && prettier ./src/**/*.js -l", |
@@ -1,7 +0,5 @@ | ||
# Push Notifications Web (BETA) | ||
# Push Notifications Web | ||
[![Build Status](https://travis-ci.org/pusher/push-notifications-web.svg?branch=master)](https://travis-ci.org/pusher/push-notifications-web) | ||
[![Twitter](https://img.shields.io/badge/twitter-@Pusher-blue.svg?style=flat)](http://twitter.com/Pusher) | ||
**Warning: This SDK is in Beta therefore there may be breaking changes in the future. In its current form, the SDK has limitations, which are explained in the [getting started guide](https://pusher.com/docs/beams/getting-started/web/sdk-integration).** | ||
This is the web SDK for the [Pusher Beams](https://pusher.com/beams) service. | ||
@@ -8,0 +6,0 @@ |
@@ -12,124 +12,137 @@ import doRequest from './do-request'; | ||
export async function init(config) { | ||
if (!config) { | ||
throw new Error('Config object required'); | ||
} | ||
const { | ||
instanceId, | ||
endpointOverride = null, | ||
serviceWorkerRegistration = null, | ||
} = config; | ||
export const RegistrationState = Object.freeze({ | ||
PERMISSION_PROMPT_REQUIRED: 'PERMISSION_PROMPT_REQUIRED', | ||
PERMISSION_GRANTED_NOT_REGISTERED_WITH_BEAMS: | ||
'PERMISSION_GRANTED_NOT_REGISTERED_WITH_BEAMS', | ||
PERMISSION_GRANTED_REGISTERED_WITH_BEAMS: | ||
'PERMISSION_GRANTED_REGISTERED_WITH_BEAMS', | ||
PERMISSION_DENIED: 'PERMISSION_DENIED', | ||
}); | ||
if (instanceId === undefined) { | ||
throw new Error('Instance ID is required'); | ||
} | ||
if (typeof instanceId !== 'string') { | ||
throw new Error('Instance ID must be a string'); | ||
} | ||
if (instanceId.length === 0) { | ||
throw new Error('Instance ID cannot be empty'); | ||
} | ||
export class Client { | ||
constructor(config) { | ||
if (!config) { | ||
throw new Error('Config object required'); | ||
} | ||
const { | ||
instanceId, | ||
endpointOverride = null, | ||
serviceWorkerRegistration = null, | ||
} = config; | ||
if (!('indexedDB' in window)) { | ||
throw new Error( | ||
'Pusher Beams does not support this browser version (IndexedDB not supported)' | ||
); | ||
} | ||
if (instanceId === undefined) { | ||
throw new Error('Instance ID is required'); | ||
} | ||
if (typeof instanceId !== 'string') { | ||
throw new Error('Instance ID must be a string'); | ||
} | ||
if (instanceId.length === 0) { | ||
throw new Error('Instance ID cannot be empty'); | ||
} | ||
if (!window.isSecureContext) { | ||
throw new Error( | ||
'Pusher Beams relies on Service Workers, which only work in secure contexts. Check that your page is being served from localhost/over HTTPS' | ||
); | ||
} | ||
if (!('indexedDB' in window)) { | ||
throw new Error( | ||
'Pusher Beams does not support this browser version (IndexedDB not supported)' | ||
); | ||
} | ||
if (!('serviceWorker' in navigator)) { | ||
throw new Error( | ||
'Pusher Beams does not support this browser version (Service Workers not supported)' | ||
); | ||
} | ||
if (!window.isSecureContext) { | ||
throw new Error( | ||
'Pusher Beams relies on Service Workers, which only work in secure contexts. Check that your page is being served from localhost/over HTTPS' | ||
); | ||
} | ||
if (!('PushManager' in window)) { | ||
throw new Error( | ||
'Pusher Beams does not support this browser version (Web Push not supported)' | ||
); | ||
} | ||
if (!('serviceWorker' in navigator)) { | ||
throw new Error( | ||
'Pusher Beams does not support this browser version (Service Workers not supported)' | ||
); | ||
} | ||
let swReg; | ||
if (serviceWorkerRegistration) { | ||
const serviceWorkerScope = serviceWorkerRegistration.scope; | ||
const currentURL = window.location.href; | ||
const scopeMatchesCurrentPage = currentURL.startsWith(serviceWorkerScope); | ||
if (!scopeMatchesCurrentPage) { | ||
if (!('PushManager' in window)) { | ||
throw new Error( | ||
`Could not initialize Pusher web push: current page not in serviceWorkerRegistration scope (${serviceWorkerScope})` | ||
'Pusher Beams does not support this browser version (Web Push not supported)' | ||
); | ||
} | ||
swReg = serviceWorkerRegistration; | ||
} else { | ||
swReg = await getServiceWorkerRegistration(); | ||
if (serviceWorkerRegistration) { | ||
const serviceWorkerScope = serviceWorkerRegistration.scope; | ||
const currentURL = window.location.href; | ||
const scopeMatchesCurrentPage = currentURL.startsWith(serviceWorkerScope); | ||
if (!scopeMatchesCurrentPage) { | ||
throw new Error( | ||
`Could not initialize Pusher web push: current page not in serviceWorkerRegistration scope (${serviceWorkerScope})` | ||
); | ||
} | ||
} | ||
this.instanceId = instanceId; | ||
this._deviceId = null; | ||
this._token = null; | ||
this._userId = null; | ||
this._serviceWorkerRegistration = serviceWorkerRegistration; | ||
this._deviceStateStore = new DeviceStateStore(instanceId); | ||
this._endpoint = endpointOverride; // Internal only | ||
this._ready = this._init(); | ||
} | ||
const deviceStateStore = new DeviceStateStore(instanceId); | ||
await deviceStateStore.connect(); | ||
async _init() { | ||
if (this._deviceId !== null) { | ||
return; | ||
} | ||
const storedToken = await deviceStateStore.getToken(); | ||
const actualToken = await getWebPushToken(swReg); | ||
await this._deviceStateStore.connect(); | ||
const pushTokenHasChanged = storedToken !== actualToken; | ||
if (!this._serviceWorkerRegistration) { | ||
this._serviceWorkerRegistration = await getServiceWorkerRegistration(); | ||
} | ||
if (pushTokenHasChanged) { | ||
// The web push subscription has changed out from underneath us. | ||
// This can happen when the user disables the web push permission | ||
// (potentially also renabling it, thereby changing the token) | ||
// | ||
// This means the SDK has effectively been stopped, so we should update | ||
// the SDK state to reflect that. | ||
await deviceStateStore.clear(); | ||
await this._detectSubscriptionChange(); | ||
this._deviceId = await this._deviceStateStore.getDeviceId(); | ||
this._token = await this._deviceStateStore.getToken(); | ||
this._userId = await this._deviceStateStore.getUserId(); | ||
} | ||
const deviceId = await deviceStateStore.getDeviceId(); | ||
const token = await deviceStateStore.getToken(); | ||
const userId = await deviceStateStore.getUserId(); | ||
// Ensure SDK is loaded and is consistent | ||
async _resolveSDKState() { | ||
await this._ready; | ||
await this._detectSubscriptionChange(); | ||
} | ||
const instance = new PushNotificationsInstance({ | ||
instanceId, | ||
deviceId, | ||
token, | ||
userId, | ||
serviceWorkerRegistration: swReg, | ||
deviceStateStore, | ||
endpointOverride, | ||
}); | ||
async _detectSubscriptionChange() { | ||
const storedToken = await this._deviceStateStore.getToken(); | ||
const actualToken = await getWebPushToken(this._serviceWorkerRegistration); | ||
const deviceExists = deviceId !== null; | ||
if (deviceExists) { | ||
try { | ||
await instance._updateDeviceMetadata(); | ||
} catch (_) { | ||
// Best effort, do nothing if this fails. | ||
const pushTokenHasChanged = storedToken !== actualToken; | ||
if (pushTokenHasChanged) { | ||
// The web push subscription has changed out from underneath us. | ||
// This can happen when the user disables the web push permission | ||
// (potentially also renabling it, thereby changing the token) | ||
// | ||
// This means the SDK has effectively been stopped, so we should update | ||
// the SDK state to reflect that. | ||
await this._deviceStateStore.clear(); | ||
this._deviceId = null; | ||
this._token = null; | ||
this._userId = null; | ||
} | ||
} | ||
return instance; | ||
} | ||
async getDeviceId() { | ||
await this._resolveSDKState(); | ||
return this._ready.then(() => this._deviceId); | ||
} | ||
class PushNotificationsInstance { | ||
constructor({ | ||
instanceId, | ||
deviceId, | ||
token, | ||
userId, | ||
serviceWorkerRegistration, | ||
deviceStateStore, | ||
endpointOverride = null, | ||
}) { | ||
this.instanceId = instanceId; | ||
this.deviceId = deviceId; | ||
this.token = token; | ||
this.userId = userId; | ||
this._serviceWorkerRegistration = serviceWorkerRegistration; | ||
this._deviceStateStore = deviceStateStore; | ||
this._endpoint = endpointOverride; // Internal only | ||
async getToken() { | ||
await this._resolveSDKState(); | ||
return this._ready.then(() => this._token); | ||
} | ||
async getUserId() { | ||
await this._resolveSDKState(); | ||
return this._ready.then(() => this._userId); | ||
} | ||
get _baseURL() { | ||
@@ -143,3 +156,3 @@ if (this._endpoint !== null) { | ||
_throwIfNotStarted(message) { | ||
if (!this.deviceId) { | ||
if (!this._deviceId) { | ||
throw new Error( | ||
@@ -152,2 +165,4 @@ `${message}. SDK not registered with Beams. Did you call .start?` | ||
async start() { | ||
await this._resolveSDKState(); | ||
if (!isSupportedBrowser()) { | ||
@@ -157,3 +172,3 @@ return this; | ||
if (this.deviceId !== null) { | ||
if (this._deviceId !== null) { | ||
return this; | ||
@@ -177,9 +192,29 @@ } | ||
this.token = token; | ||
this.deviceId = deviceId; | ||
this._token = token; | ||
this._deviceId = deviceId; | ||
return this; | ||
} | ||
async getRegistrationState() { | ||
await this._resolveSDKState(); | ||
if (Notification.permission === 'denied') { | ||
return RegistrationState.PERMISSION_DENIED; | ||
} | ||
if (Notification.permission === 'granted' && this._deviceId !== null) { | ||
return RegistrationState.PERMISSION_GRANTED_REGISTERED_WITH_BEAMS; | ||
} | ||
if (Notification.permission === 'granted' && this._deviceId === null) { | ||
return RegistrationState.PERMISSION_GRANTED_NOT_REGISTERED_WITH_BEAMS; | ||
} | ||
return RegistrationState.PERMISSION_PROMPT_REQUIRED; | ||
} | ||
async addDeviceInterest(interest) { | ||
await this._resolveSDKState(); | ||
this._throwIfNotStarted('Could not add Device Interest'); | ||
validateInterestName(interest); | ||
@@ -189,3 +224,3 @@ | ||
this.instanceId | ||
)}/devices/web/${this.deviceId}/interests/${encodeURIComponent(interest)}`; | ||
)}/devices/web/${this._deviceId}/interests/${encodeURIComponent(interest)}`; | ||
const options = { | ||
@@ -199,3 +234,5 @@ method: 'POST', | ||
async removeDeviceInterest(interest) { | ||
await this._resolveSDKState(); | ||
this._throwIfNotStarted('Could not remove Device Interest'); | ||
validateInterestName(interest); | ||
@@ -205,3 +242,3 @@ | ||
this.instanceId | ||
)}/devices/web/${this.deviceId}/interests/${encodeURIComponent(interest)}`; | ||
)}/devices/web/${this._deviceId}/interests/${encodeURIComponent(interest)}`; | ||
const options = { | ||
@@ -215,2 +252,3 @@ method: 'DELETE', | ||
async getDeviceInterests() { | ||
await this._resolveSDKState(); | ||
this._throwIfNotStarted('Could not get Device Interests'); | ||
@@ -220,3 +258,3 @@ | ||
this.instanceId | ||
)}/devices/web/${this.deviceId}/interests`; | ||
)}/devices/web/${this._deviceId}/interests`; | ||
const options = { | ||
@@ -230,2 +268,3 @@ method: 'GET', | ||
async setDeviceInterests(interests) { | ||
await this._resolveSDKState(); | ||
this._throwIfNotStarted('Could not set Device Interests'); | ||
@@ -253,3 +292,3 @@ | ||
this.instanceId | ||
)}/devices/web/${this.deviceId}/interests`; | ||
)}/devices/web/${this._deviceId}/interests`; | ||
const options = { | ||
@@ -266,3 +305,5 @@ method: 'PUT', | ||
async clearDeviceInterests() { | ||
await this._resolveSDKState(); | ||
this._throwIfNotStarted('Could not clear Device Interests'); | ||
await this.setDeviceInterests([]); | ||
@@ -272,2 +313,4 @@ } | ||
async setUserId(userId, tokenProvider) { | ||
await this._resolveSDKState(); | ||
if (!isSupportedBrowser()) { | ||
@@ -277,3 +320,3 @@ return; | ||
if (this.deviceId === null) { | ||
if (this._deviceId === null) { | ||
const error = new Error('.start must be called before .setUserId'); | ||
@@ -288,3 +331,3 @@ return Promise.reject(error); | ||
} | ||
if (this.userId !== null && this.userId !== userId) { | ||
if (this._userId !== null && this._userId !== userId) { | ||
throw new Error('Changing the `userId` is not allowed.'); | ||
@@ -295,3 +338,3 @@ } | ||
this.instanceId | ||
)}/devices/web/${this.deviceId}/user`; | ||
)}/devices/web/${this._deviceId}/user`; | ||
@@ -308,3 +351,3 @@ const { token: beamsAuthToken } = await tokenProvider.fetchToken(userId); | ||
this.userId = userId; | ||
this._userId = userId; | ||
return this._deviceStateStore.setUserId(userId); | ||
@@ -314,2 +357,4 @@ } | ||
async stop() { | ||
await this._resolveSDKState(); | ||
if (!isSupportedBrowser()) { | ||
@@ -319,3 +364,3 @@ return; | ||
if (this.deviceId === null) { | ||
if (this._deviceId === null) { | ||
return; | ||
@@ -328,5 +373,5 @@ } | ||
this.deviceId = null; | ||
this.token = null; | ||
this.userId = null; | ||
this._deviceId = null; | ||
this._token = null; | ||
this._userId = null; | ||
} | ||
@@ -395,3 +440,3 @@ | ||
this.instanceId | ||
)}/devices/web/${encodeURIComponent(this.deviceId)}`; | ||
)}/devices/web/${encodeURIComponent(this._deviceId)}`; | ||
@@ -417,3 +462,3 @@ const options = { method: 'DELETE', path }; | ||
this.instanceId | ||
)}/devices/web/${this.deviceId}/metadata`; | ||
)}/devices/web/${this._deviceId}/metadata`; | ||
@@ -508,3 +553,3 @@ const metadata = { | ||
console.warn( | ||
'Whilst in Beta, Pusher Web Push Notifications supports Chrome, Firefox, Edge and Opera.' | ||
'Pusher Web Push Notifications supports Chrome, Firefox, Edge and Opera.' | ||
); | ||
@@ -511,0 +556,0 @@ } |
import * as PusherPushNotifications from './push-notifications'; | ||
import { makeDeviceStateStore } from '../test-utils/fake-device-state-store'; | ||
@@ -12,8 +13,4 @@ const DUMMY_PUSH_SUBSCRIPTION = { foo: 'bar' }; | ||
test('should be a function', () => { | ||
expect(typeof PusherPushNotifications.init).toBe('function'); | ||
}); | ||
test('will throw if there is no config object given', () => { | ||
return expect(PusherPushNotifications.init()).rejects.toThrow( | ||
return expect(() => new PusherPushNotifications.Client()).toThrow( | ||
'Config object required' | ||
@@ -24,3 +21,3 @@ ); | ||
test('will throw if there is no instance ID specified', () => { | ||
return expect(PusherPushNotifications.init({})).rejects.toThrow( | ||
return expect(() => new PusherPushNotifications.Client({})).toThrow( | ||
'Instance ID is required' | ||
@@ -32,5 +29,5 @@ ); | ||
const instanceId = null; | ||
return expect(PusherPushNotifications.init({ instanceId })).rejects.toThrow( | ||
'Instance ID must be a string' | ||
); | ||
return expect( | ||
() => new PusherPushNotifications.Client({ instanceId }) | ||
).toThrow('Instance ID must be a string'); | ||
}); | ||
@@ -40,5 +37,5 @@ | ||
const instanceId = ''; | ||
return expect(PusherPushNotifications.init({ instanceId })).rejects.toThrow( | ||
'Instance ID cannot be empty' | ||
); | ||
return expect( | ||
() => new PusherPushNotifications.Client({ instanceId }) | ||
).toThrow('Instance ID cannot be empty'); | ||
}); | ||
@@ -49,5 +46,5 @@ | ||
const instanceId = 'df3c1965-e870-4bd6-8d75-fea56b26335f'; | ||
return expect(PusherPushNotifications.init({ instanceId })).rejects.toThrow( | ||
'IndexedDB not supported' | ||
); | ||
return expect( | ||
() => new PusherPushNotifications.Client({ instanceId }) | ||
).toThrow('IndexedDB not supported'); | ||
}); | ||
@@ -58,3 +55,5 @@ | ||
const instanceId = 'df3c1965-e870-4bd6-8d75-fea56b26335f'; | ||
return expect(PusherPushNotifications.init({ instanceId })).rejects.toThrow( | ||
return expect( | ||
() => new PusherPushNotifications.Client({ instanceId }) | ||
).toThrow( | ||
'Pusher Beams relies on Service Workers, which only work in secure contexts' | ||
@@ -67,5 +66,5 @@ ); | ||
const instanceId = 'df3c1965-e870-4bd6-8d75-fea56b26335f'; | ||
return expect(PusherPushNotifications.init({ instanceId })).rejects.toThrow( | ||
'Service Workers not supported' | ||
); | ||
return expect( | ||
() => new PusherPushNotifications.Client({ instanceId }) | ||
).toThrow('Service Workers not supported'); | ||
}); | ||
@@ -76,5 +75,5 @@ | ||
const instanceId = 'df3c1965-e870-4bd6-8d75-fea56b26335f'; | ||
return expect(PusherPushNotifications.init({ instanceId })).rejects.toThrow( | ||
'Web Push not supported' | ||
); | ||
return expect( | ||
() => new PusherPushNotifications.Client({ instanceId }) | ||
).toThrow('Web Push not supported'); | ||
}); | ||
@@ -96,8 +95,11 @@ | ||
return PusherPushNotifications.init({ instanceId }).then(beamsClient => { | ||
expect(beamsClient.deviceId).toEqual( | ||
'web-1db66b8a-f51f-49de-b225-72591535c855' | ||
); | ||
expect(beamsClient.token).toEqual(ENCODED_DUMMY_PUSH_SUBSCRIPTION); | ||
expect(beamsClient.userId).toEqual('alice'); | ||
const beamsClient = new PusherPushNotifications.Client({ instanceId }); | ||
return Promise.all([ | ||
beamsClient.getDeviceId(), | ||
beamsClient.getToken(), | ||
beamsClient.getUserId(), | ||
]).then(([deviceId, token, userId]) => { | ||
expect(deviceId).toEqual('web-1db66b8a-f51f-49de-b225-72591535c855'); | ||
expect(token).toEqual(ENCODED_DUMMY_PUSH_SUBSCRIPTION); | ||
expect(userId).toEqual('alice'); | ||
}); | ||
@@ -138,19 +140,18 @@ }); | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId, | ||
}) | ||
.then(beamsClient => beamsClient.addDeviceInterest(interest)) | ||
.then(() => { | ||
expect(mockDoRequest.mock.calls.length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0].length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0][0]).toEqual({ | ||
method: 'POST', | ||
path: [ | ||
'https://df3c1965-e870-4bd6-8d75-fea56b26335f.pushnotifications.pusher.com', | ||
'/device_api/v1/instances/df3c1965-e870-4bd6-8d75-fea56b26335f', | ||
'/devices/web/web-1db66b8a-f51f-49de-b225-72591535c855', | ||
'/interests/donuts', | ||
].join(''), | ||
}); | ||
}); | ||
return beamsClient.addDeviceInterest(interest).then(() => { | ||
expect(mockDoRequest.mock.calls.length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0].length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0][0]).toEqual({ | ||
method: 'POST', | ||
path: [ | ||
'https://df3c1965-e870-4bd6-8d75-fea56b26335f.pushnotifications.pusher.com', | ||
'/device_api/v1/instances/df3c1965-e870-4bd6-8d75-fea56b26335f', | ||
'/devices/web/web-1db66b8a-f51f-49de-b225-72591535c855', | ||
'/interests/donuts', | ||
].join(''), | ||
}); | ||
}); | ||
}); | ||
@@ -161,5 +162,5 @@ | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.addDeviceInterest()) | ||
}).addDeviceInterest() | ||
).rejects.toThrow('Interest name is required'); | ||
@@ -172,5 +173,5 @@ }); | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.addDeviceInterest(interest)) | ||
}).addDeviceInterest(interest) | ||
).rejects.toThrow('Interest false is not a string'); | ||
@@ -186,5 +187,5 @@ }); | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.addDeviceInterest(interest)) | ||
}).addDeviceInterest(interest) | ||
).rejects.toThrow('Interest is longer than the maximum of 164 chars'); | ||
@@ -197,5 +198,5 @@ }); | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.addDeviceInterest(interest)) | ||
}).addDeviceInterest(interest) | ||
).rejects.toThrow('contains a forbidden character'); | ||
@@ -215,5 +216,5 @@ }); | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.addDeviceInterest(interest)) | ||
}).addDeviceInterest(interest) | ||
).rejects.toThrow('SDK not registered with Beams. Did you call .start?'); | ||
@@ -233,19 +234,16 @@ }); | ||
return PusherPushNotifications.init({ | ||
instanceId, | ||
}) | ||
.then(beamsClient => beamsClient.removeDeviceInterest(interest)) | ||
.then(() => { | ||
expect(mockDoRequest.mock.calls.length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0].length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0][0]).toEqual({ | ||
method: 'DELETE', | ||
path: [ | ||
'https://df3c1965-e870-4bd6-8d75-fea56b26335f.pushnotifications.pusher.com', | ||
'/device_api/v1/instances/df3c1965-e870-4bd6-8d75-fea56b26335f', | ||
'/devices/web/web-1db66b8a-f51f-49de-b225-72591535c855', | ||
'/interests/donuts', | ||
].join(''), | ||
}); | ||
const beamsClient = new PusherPushNotifications.Client({ instanceId }); | ||
return beamsClient.removeDeviceInterest(interest).then(() => { | ||
expect(mockDoRequest.mock.calls.length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0].length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0][0]).toEqual({ | ||
method: 'DELETE', | ||
path: [ | ||
'https://df3c1965-e870-4bd6-8d75-fea56b26335f.pushnotifications.pusher.com', | ||
'/device_api/v1/instances/df3c1965-e870-4bd6-8d75-fea56b26335f', | ||
'/devices/web/web-1db66b8a-f51f-49de-b225-72591535c855', | ||
'/interests/donuts', | ||
].join(''), | ||
}); | ||
}); | ||
}); | ||
@@ -256,5 +254,5 @@ | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.removeDeviceInterest()) | ||
}).removeDeviceInterest() | ||
).rejects.toThrow('Interest name is required'); | ||
@@ -267,5 +265,5 @@ }); | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.removeDeviceInterest(interest)) | ||
}).removeDeviceInterest(interest) | ||
).rejects.toThrow('Interest false is not a string'); | ||
@@ -281,5 +279,5 @@ }); | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.removeDeviceInterest(interest)) | ||
}).removeDeviceInterest(interest) | ||
).rejects.toThrow('Interest is longer than the maximum of 164 chars'); | ||
@@ -292,5 +290,5 @@ }); | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.removeDeviceInterest(interest)) | ||
}).removeDeviceInterest(interest) | ||
).rejects.toThrow('contains a forbidden character'); | ||
@@ -310,5 +308,5 @@ }); | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.removeDeviceInterest(interest)) | ||
}).removeDeviceInterest(interest) | ||
).rejects.toThrow('SDK not registered with Beams. Did you call .start?'); | ||
@@ -332,20 +330,19 @@ }); | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId, | ||
}) | ||
.then(beamsClient => beamsClient.getDeviceInterests()) | ||
.then(interests => { | ||
expect(mockDoRequest.mock.calls.length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0].length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0][0]).toEqual({ | ||
method: 'GET', | ||
path: [ | ||
'https://df3c1965-e870-4bd6-8d75-fea56b26335f.pushnotifications.pusher.com', | ||
'/device_api/v1/instances/df3c1965-e870-4bd6-8d75-fea56b26335f', | ||
'/devices/web/web-1db66b8a-f51f-49de-b225-72591535c855', | ||
'/interests', | ||
].join(''), | ||
}); | ||
expect(interests).toEqual(['donuts']); | ||
}); | ||
return beamsClient.getDeviceInterests().then(interests => { | ||
expect(mockDoRequest.mock.calls.length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0].length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0][0]).toEqual({ | ||
method: 'GET', | ||
path: [ | ||
'https://df3c1965-e870-4bd6-8d75-fea56b26335f.pushnotifications.pusher.com', | ||
'/device_api/v1/instances/df3c1965-e870-4bd6-8d75-fea56b26335f', | ||
'/devices/web/web-1db66b8a-f51f-49de-b225-72591535c855', | ||
'/interests', | ||
].join(''), | ||
}); | ||
expect(interests).toEqual(['donuts']); | ||
}); | ||
}); | ||
@@ -363,5 +360,5 @@ | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.getDeviceInterests()) | ||
}).getDeviceInterests() | ||
).rejects.toThrow('SDK not registered with Beams. Did you call .start?'); | ||
@@ -381,22 +378,21 @@ }); | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId, | ||
}) | ||
.then(beamsClient => beamsClient.setDeviceInterests(interests)) | ||
.then(() => { | ||
expect(mockDoRequest.mock.calls.length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0].length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0][0].method).toEqual('PUT'); | ||
expect(mockDoRequest.mock.calls[0][0].path).toEqual( | ||
[ | ||
'https://df3c1965-e870-4bd6-8d75-fea56b26335f.pushnotifications.pusher.com', | ||
'/device_api/v1/instances/df3c1965-e870-4bd6-8d75-fea56b26335f', | ||
'/devices/web/web-1db66b8a-f51f-49de-b225-72591535c855', | ||
'/interests', | ||
].join('') | ||
); | ||
expect(mockDoRequest.mock.calls[0][0].body.interests.sort()).toEqual( | ||
[...interests].sort() | ||
); | ||
}); | ||
}); | ||
return beamsClient.setDeviceInterests(interests).then(() => { | ||
expect(mockDoRequest.mock.calls.length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0].length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0][0].method).toEqual('PUT'); | ||
expect(mockDoRequest.mock.calls[0][0].path).toEqual( | ||
[ | ||
'https://df3c1965-e870-4bd6-8d75-fea56b26335f.pushnotifications.pusher.com', | ||
'/device_api/v1/instances/df3c1965-e870-4bd6-8d75-fea56b26335f', | ||
'/devices/web/web-1db66b8a-f51f-49de-b225-72591535c855', | ||
'/interests', | ||
].join('') | ||
); | ||
expect(mockDoRequest.mock.calls[0][0].body.interests.sort()).toEqual( | ||
[...interests].sort() | ||
); | ||
}); | ||
}); | ||
@@ -415,22 +411,21 @@ | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId, | ||
}) | ||
.then(beamsClient => beamsClient.setDeviceInterests(interests)) | ||
.then(() => { | ||
expect(mockDoRequest.mock.calls.length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0].length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0][0].method).toEqual('PUT'); | ||
expect(mockDoRequest.mock.calls[0][0].path).toEqual( | ||
[ | ||
'https://df3c1965-e870-4bd6-8d75-fea56b26335f.pushnotifications.pusher.com', | ||
'/device_api/v1/instances/df3c1965-e870-4bd6-8d75-fea56b26335f', | ||
'/devices/web/web-1db66b8a-f51f-49de-b225-72591535c855', | ||
'/interests', | ||
].join('') | ||
); | ||
expect(mockDoRequest.mock.calls[0][0].body.interests.sort()).toEqual( | ||
expectedInterests.sort() | ||
); | ||
}); | ||
}); | ||
return beamsClient.setDeviceInterests(interests).then(() => { | ||
expect(mockDoRequest.mock.calls.length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0].length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0][0].method).toEqual('PUT'); | ||
expect(mockDoRequest.mock.calls[0][0].path).toEqual( | ||
[ | ||
'https://df3c1965-e870-4bd6-8d75-fea56b26335f.pushnotifications.pusher.com', | ||
'/device_api/v1/instances/df3c1965-e870-4bd6-8d75-fea56b26335f', | ||
'/devices/web/web-1db66b8a-f51f-49de-b225-72591535c855', | ||
'/interests', | ||
].join('') | ||
); | ||
expect(mockDoRequest.mock.calls[0][0].body.interests.sort()).toEqual( | ||
expectedInterests.sort() | ||
); | ||
}); | ||
}); | ||
@@ -441,5 +436,5 @@ | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.setDeviceInterests()) | ||
}).setDeviceInterests() | ||
).rejects.toThrow('interests argument is required'); | ||
@@ -452,5 +447,5 @@ }); | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.setDeviceInterests(interests)) | ||
}).setDeviceInterests(interests) | ||
).rejects.toThrow('interests argument must be an array'); | ||
@@ -468,5 +463,5 @@ }); | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.setDeviceInterests(interests)) | ||
}).setDeviceInterests(interests) | ||
).rejects.toThrow( | ||
@@ -483,5 +478,5 @@ `Number of interests (${maxInterests + | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.setDeviceInterests(interests)) | ||
}).setDeviceInterests(interests) | ||
).rejects.toThrow('Interest false is not a string'); | ||
@@ -498,5 +493,5 @@ }); | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.setDeviceInterests(interests)) | ||
}).setDeviceInterests(interests) | ||
).rejects.toThrow('longer than the maximum of 164 chars'); | ||
@@ -510,5 +505,5 @@ }); | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.setDeviceInterests(interests)) | ||
}).setDeviceInterests(interests) | ||
).rejects.toThrow( | ||
@@ -529,5 +524,5 @@ 'interest "bad|interest" contains a forbidden character' | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.setDeviceInterests([])) | ||
}).setDeviceInterests([]) | ||
).rejects.toThrow('SDK not registered with Beams. Did you call .start?'); | ||
@@ -546,20 +541,19 @@ }); | ||
return PusherPushNotifications.init({ | ||
const beamsClient = new PusherPushNotifications.Client({ | ||
instanceId, | ||
}) | ||
.then(beamsClient => beamsClient.clearDeviceInterests()) | ||
.then(() => { | ||
expect(mockDoRequest.mock.calls.length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0].length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0][0]).toEqual({ | ||
method: 'PUT', | ||
path: [ | ||
'https://df3c1965-e870-4bd6-8d75-fea56b26335f.pushnotifications.pusher.com', | ||
'/device_api/v1/instances/df3c1965-e870-4bd6-8d75-fea56b26335f', | ||
'/devices/web/web-1db66b8a-f51f-49de-b225-72591535c855', | ||
'/interests', | ||
].join(''), | ||
body: { interests: [] }, | ||
}); | ||
}); | ||
return beamsClient.clearDeviceInterests().then(() => { | ||
expect(mockDoRequest.mock.calls.length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0].length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0][0]).toEqual({ | ||
method: 'PUT', | ||
path: [ | ||
'https://df3c1965-e870-4bd6-8d75-fea56b26335f.pushnotifications.pusher.com', | ||
'/device_api/v1/instances/df3c1965-e870-4bd6-8d75-fea56b26335f', | ||
'/devices/web/web-1db66b8a-f51f-49de-b225-72591535c855', | ||
'/interests', | ||
].join(''), | ||
body: { interests: [] }, | ||
}); | ||
}); | ||
}); | ||
@@ -577,5 +571,5 @@ | ||
return expect( | ||
PusherPushNotifications.init({ | ||
new PusherPushNotifications.Client({ | ||
instanceId, | ||
}).then(beamsClient => beamsClient.clearDeviceInterests()) | ||
}).clearDeviceInterests() | ||
).rejects.toThrow('SDK not registered with Beams. Did you call .start?'); | ||
@@ -586,2 +580,143 @@ }); | ||
describe('.getRegistrationState', () => { | ||
const instanceId = 'df3c1965-e870-4bd6-8d75-fea56b26335f'; | ||
describe('if SDK is started', () => { | ||
let devicestatestore = require('./device-state-store'); | ||
beforeEach(() => { | ||
devicestatestore.default = makeDeviceStateStore({ | ||
deviceId: 'web-1db66b8a-f51f-49de-b225-72591535c855', | ||
token: ENCODED_DUMMY_PUSH_SUBSCRIPTION, | ||
userId: null, | ||
}); | ||
}); | ||
afterEach(() => { | ||
jest.resetModules(); | ||
devicestatestore = require('./device-state-store'); | ||
}); | ||
test('should return PERMISSION_GRANTED_REGISTERED_WITH_BEAMS if browser permission is granted', () => { | ||
setUpGlobals({ notificationPermission: 'granted' }); | ||
let beamsClient = new PusherPushNotifications.Client({ | ||
instanceId, | ||
}); | ||
return beamsClient.getRegistrationState().then(state => { | ||
expect(state).toEqual( | ||
PusherPushNotifications.RegistrationState | ||
.PERMISSION_GRANTED_REGISTERED_WITH_BEAMS | ||
); | ||
}); | ||
}); | ||
}); | ||
describe('if SDK is not started', () => { | ||
let devicestatestore = require('./device-state-store'); | ||
beforeEach(() => { | ||
devicestatestore.default = makeDeviceStateStore({ | ||
deviceId: null, | ||
token: null, | ||
userId: null, | ||
}); | ||
}); | ||
afterEach(() => { | ||
jest.resetModules(); | ||
devicestatestore = require('./device-state-store'); | ||
tearDownGlobals(); | ||
}); | ||
test('should return PERMISSION_DENIED if browser permission is denied', () => { | ||
setUpGlobals({ notificationPermission: 'denied' }); | ||
let beamsClient = new PusherPushNotifications.Client({ | ||
instanceId, | ||
}); | ||
return beamsClient.getRegistrationState().then(state => { | ||
expect(state).toEqual( | ||
PusherPushNotifications.RegistrationState.PERMISSION_DENIED | ||
); | ||
}); | ||
}); | ||
test('should return PERMISSION_PROMPT_REQUIRED if browser permission is default', () => { | ||
setUpGlobals({ notificationPermission: 'default' }); | ||
let beamsClient = new PusherPushNotifications.Client({ | ||
instanceId, | ||
}); | ||
return beamsClient.getRegistrationState().then(state => { | ||
expect(state).toEqual( | ||
PusherPushNotifications.RegistrationState.PERMISSION_PROMPT_REQUIRED | ||
); | ||
}); | ||
}); | ||
test('should return PERMISSION_GRANTED_NOT_REGISTERED_WITH_BEAMS if browser permission is granted', () => { | ||
setUpGlobals({ notificationPermission: 'granted' }); | ||
let beamsClient = new PusherPushNotifications.Client({ | ||
instanceId, | ||
}); | ||
return beamsClient.getRegistrationState().then(state => { | ||
expect(state).toEqual( | ||
PusherPushNotifications.RegistrationState | ||
.PERMISSION_GRANTED_NOT_REGISTERED_WITH_BEAMS | ||
); | ||
}); | ||
}); | ||
}); | ||
}); | ||
describe('SDK state', () => { | ||
afterEach(() => { | ||
jest.resetModules(); | ||
tearDownGlobals(); | ||
}); | ||
test('should be reset if subscription changes', () => { | ||
const PusherPushNotifications = require('./push-notifications'); | ||
const devicestatestore = require('./device-state-store'); | ||
let subscription = DUMMY_PUSH_SUBSCRIPTION; | ||
setUpGlobals({ | ||
getSWSubscription: () => { | ||
return Promise.resolve(subscription); | ||
}, | ||
}); | ||
const instanceId = 'df3c1965-e870-4bd6-8d75-fea56b26335f'; | ||
let deviceId = 'web-1db66b8a-f51f-49de-b225-72591535c855'; | ||
let newSubscription = { another: 'subscription' }; | ||
expect(newSubscription).not.toEqual(DUMMY_PUSH_SUBSCRIPTION); | ||
devicestatestore.default = makeDeviceStateStore({ | ||
deviceId, | ||
token: ENCODED_DUMMY_PUSH_SUBSCRIPTION, | ||
userId: 'alice', | ||
}); | ||
let beamsClient = new PusherPushNotifications.Client({ | ||
instanceId, | ||
}); | ||
return beamsClient | ||
.getDeviceId() | ||
.then(returnedDeviceId => { | ||
// Device ID should have been set | ||
return expect(returnedDeviceId).toEqual(deviceId); | ||
}) | ||
.then(() => { | ||
// Change subscription | ||
subscription = newSubscription; | ||
}) | ||
.then(() => beamsClient.getDeviceId()) | ||
.then(deviceId => { | ||
// Device ID should have been cleared | ||
return expect(deviceId).toBeNull(); | ||
}); | ||
}); | ||
}); | ||
const setUpGlobals = ({ | ||
@@ -592,2 +727,4 @@ indexedDBSupport = true, | ||
isSecureContext = true, | ||
notificationPermission = 'default', | ||
getSWSubscription = () => Promise.resolve(DUMMY_PUSH_SUBSCRIPTION), | ||
}) => { | ||
@@ -602,3 +739,3 @@ if (indexedDBSupport) { | ||
pushManager: { | ||
getSubscription: () => Promise.resolve(DUMMY_PUSH_SUBSCRIPTION), | ||
getSubscription: getSWSubscription, | ||
}, | ||
@@ -611,2 +748,5 @@ }); | ||
global.window.isSecureContext = isSecureContext; | ||
global.Notification = {}; | ||
global.Notification.permission = notificationPermission; | ||
}; | ||
@@ -618,51 +758,4 @@ | ||
delete global.navigator.serviceWorker; | ||
delete global.window.isSecureContext; | ||
delete global.Notification; | ||
}; | ||
const makeDeviceStateStore = ({ deviceId, token, userId }) => { | ||
class FakeDeviceStateStore { | ||
constructor(instanceId) { | ||
this.instanceId = instanceId; | ||
this._deviceId = null; | ||
this._token = null; | ||
this._userId = null; | ||
} | ||
async connect() { | ||
this._deviceId = deviceId || null; | ||
this._token = token || null; | ||
this._userId = userId || null; | ||
} | ||
async clear() { | ||
this._deviceId = null; | ||
this._token = null; | ||
this._userId = null; | ||
} | ||
async getDeviceId() { | ||
return this._deviceId; | ||
} | ||
async setDeviceId(deviceId) { | ||
this._deviceId = deviceId; | ||
} | ||
async getToken() { | ||
return this._token; | ||
} | ||
async setToken(token) { | ||
this._token = token; | ||
} | ||
async getUserId() { | ||
return this._userId; | ||
} | ||
async setUserId(userId) { | ||
this._userId = userId; | ||
} | ||
} | ||
return FakeDeviceStateStore; | ||
}; |
/* eslint-env serviceworker */ | ||
import doRequest from './do-request'; | ||
import DeviceStateStore from './device-state-store'; | ||
self.PusherPushNotifications = { | ||
endpointOverride: null, | ||
onNotificationReceived: null, | ||
_endpoint: instanceId => | ||
self.PusherPushNotifications.endpointOverride | ||
? self.PusherPushNotifications.endpointOverride | ||
: `https://${instanceId}.pushnotifications.pusher.com`, | ||
_getVisibleClient: () => | ||
self.clients | ||
.matchAll({ | ||
type: 'window', | ||
includeUncontrolled: true, | ||
}) | ||
.then(clients => clients.find(c => c.visibilityState === 'visible')), | ||
_hasVisibleClient: () => | ||
self.PusherPushNotifications._getVisibleClient().then( | ||
client => client !== undefined | ||
), | ||
_getFocusedClient: () => | ||
self.clients | ||
.matchAll({ | ||
type: 'window', | ||
includeUncontrolled: true, | ||
}) | ||
.then(clients => clients.find(c => c.focused === true)), | ||
_hasFocusedClient: () => | ||
self.PusherPushNotifications._getFocusedClient().then( | ||
client => client !== undefined | ||
), | ||
reportEvent: async ({ eventType, pusherMetadata }) => { | ||
const { | ||
instanceId, | ||
publishId, | ||
hasDisplayableContent, | ||
hasData, | ||
} = pusherMetadata; | ||
if (!instanceId || !publishId) { | ||
// Can't report this notification, fail silently. | ||
return; | ||
} | ||
const deviceStateStore = new DeviceStateStore(instanceId); | ||
await deviceStateStore.connect(); | ||
const deviceId = await deviceStateStore.getDeviceId(); | ||
const userId = (await deviceStateStore.getUserId()) || null; | ||
const appInBackground = !(await self.PusherPushNotifications._hasVisibleClient()); | ||
const path = `${self.PusherPushNotifications._endpoint( | ||
instanceId | ||
)}/reporting_api/v2/instances/${instanceId}/events`; | ||
const options = { | ||
method: 'POST', | ||
path, | ||
body: { | ||
publishId, | ||
event: eventType, | ||
deviceId, | ||
userId, | ||
timestampSecs: Date.now() / 1000, | ||
appInBackground, | ||
hasDisplayableContent, | ||
hasData, | ||
}, | ||
}; | ||
try { | ||
await doRequest(options); | ||
} catch (_) { | ||
// Reporting is best effort, so we do nothing. | ||
} | ||
}, | ||
}; | ||
@@ -18,2 +99,8 @@ | ||
// Report analytics event, best effort | ||
self.PusherPushNotifications.reportEvent({ | ||
eventType: 'delivery', | ||
pusherMetadata: payload.data.pusher, | ||
}); | ||
const customerPayload = { ...payload }; | ||
@@ -28,3 +115,12 @@ const customerData = {}; | ||
const handleNotification = payload => { | ||
const handleNotification = async payload => { | ||
const hideNotificationIfSiteHasFocus = | ||
payload.notification.hide_notification_if_site_has_focus === true; | ||
if ( | ||
hideNotificationIfSiteHasFocus && | ||
(await self.PusherPushNotifications._hasFocusedClient()) | ||
) { | ||
return; | ||
} | ||
const title = payload.notification.title || ''; | ||
@@ -59,2 +155,8 @@ const body = payload.notification.body || ''; | ||
if (isPusherNotification) { | ||
// Report analytics event, best effort | ||
self.PusherPushNotifications.reportEvent({ | ||
eventType: 'open', | ||
pusherMetadata: payload.data.pusher, | ||
}); | ||
if (payload.notification.deep_link) { | ||
@@ -61,0 +163,0 @@ e.waitUntil(clients.openWindow(payload.notification.deep_link)); |
@@ -0,4 +1,16 @@ | ||
import { makeDeviceStateStore } from '../test-utils/fake-device-state-store'; | ||
const ASYNC_TEST_WAIT_MS = 100; | ||
const TEST_INSTANCE_ID = 'some-instance-id'; | ||
const TEST_PUBLISH_ID = 'some-publish-id'; | ||
const TEST_NOTIFICATION_TITLE = 'Hi!'; | ||
const TEST_NOTIFICATION_BODY = 'This is a test notification!'; | ||
const TEST_NOTIFICATION_ICON = 'an-icon.png'; | ||
let listeners = {}; | ||
let shownNotifications = []; | ||
let openedWindows = []; | ||
let clients = []; | ||
let now = new Date('2000-01-01T00:00:00Z'); | ||
@@ -9,2 +21,3 @@ beforeEach(() => { | ||
openedWindows = []; | ||
clients = []; | ||
@@ -20,7 +33,26 @@ global.addEventListener = (name, func) => { | ||
openWindow: url => openedWindows.push(url), | ||
matchAll: () => Promise.resolve(clients), | ||
}; | ||
global.Date.now = () => now.getTime(); | ||
jest.resetModules(); | ||
// Mock out IO modules | ||
const devicestatestore = require('./device-state-store'); | ||
devicestatestore.default = makeDeviceStateStore({ | ||
deviceId: 'web-1db66b8a-f51f-49de-b225-72591535c855', | ||
token: 'some-token', | ||
userId: 'alice', | ||
}); | ||
const dorequest = require('./do-request'); | ||
dorequest.default = () => Promise.resolve('ok'); | ||
}); | ||
afterEach(() => { | ||
// Wait for any async operations to complete | ||
// This is horrible, but we we want to do open/delivery tracking without | ||
// blocking the callbacks this will have to do. | ||
return new Promise(resolve => setTimeout(resolve, ASYNC_TEST_WAIT_MS)); | ||
}); | ||
describe('SW should ignore notification when', () => { | ||
@@ -60,16 +92,3 @@ test.each([ | ||
// Given a push event that comes from Pusher | ||
const pushEvent = makePushEvent(` | ||
{ | ||
"notification": { | ||
"title": "Hi!", | ||
"body": "This is a notification!", | ||
"icon": "my-icon.png" | ||
}, | ||
"data": { | ||
"pusher": { | ||
"publishId": "some-publish-id" | ||
} | ||
} | ||
} | ||
`); | ||
const pushEvent = makeBeamsPushEvent({}); | ||
@@ -86,16 +105,19 @@ // When the push listener is called | ||
expect(shownNotifications[0]).toEqual({ | ||
title: 'Hi!', | ||
title: TEST_NOTIFICATION_TITLE, | ||
options: { | ||
icon: 'my-icon.png', | ||
body: 'This is a notification!', | ||
icon: TEST_NOTIFICATION_ICON, | ||
body: TEST_NOTIFICATION_BODY, | ||
data: { | ||
pusherPayload: { | ||
notification: { | ||
title: 'Hi!', | ||
body: 'This is a notification!', | ||
icon: 'my-icon.png', | ||
title: TEST_NOTIFICATION_TITLE, | ||
body: TEST_NOTIFICATION_BODY, | ||
icon: TEST_NOTIFICATION_ICON, | ||
}, | ||
data: { | ||
pusher: { | ||
publishId: 'some-publish-id', | ||
instanceId: TEST_INSTANCE_ID, | ||
publishId: TEST_PUBLISH_ID, | ||
hasDisplayableContent: true, | ||
hasData: false, | ||
}, | ||
@@ -114,16 +136,3 @@ }, | ||
// Given a push event that comes from Pusher | ||
const pushEvent = makePushEvent(` | ||
{ | ||
"notification": { | ||
"title": "Hi!", | ||
"body": "This is a notification!", | ||
"icon": "my-icon.png" | ||
}, | ||
"data": { | ||
"pusher": { | ||
"publishId": "some-publish-id" | ||
} | ||
} | ||
} | ||
`); | ||
const pushEvent = makeBeamsPushEvent({}); | ||
@@ -155,16 +164,3 @@ // And an onNotificationReceived had been set | ||
// Given a push event that comes from Pusher | ||
const pushEvent = makePushEvent(` | ||
{ | ||
"notification": { | ||
"title": "Hi!", | ||
"body": "This is a notification!", | ||
"icon": "my-icon.png" | ||
}, | ||
"data": { | ||
"pusher": { | ||
"publishId": "some-publish-id" | ||
} | ||
} | ||
} | ||
`); | ||
const pushEvent = makeBeamsPushEvent({}); | ||
@@ -187,5 +183,5 @@ // And an onNotificationReceived had been set | ||
notification: { | ||
title: 'Hi!', | ||
body: 'This is a notification!', | ||
icon: 'my-icon.png', | ||
title: TEST_NOTIFICATION_TITLE, | ||
body: TEST_NOTIFICATION_BODY, | ||
icon: TEST_NOTIFICATION_ICON, | ||
}, | ||
@@ -205,16 +201,3 @@ data: {}, // Pusher namespace should be stripped | ||
// Given a push event that comes from Pusher | ||
const pushEvent = makePushEvent(` | ||
{ | ||
"notification": { | ||
"title": "Hi!", | ||
"body": "This is a notification!", | ||
"icon": "my-icon.png" | ||
}, | ||
"data": { | ||
"pusher": { | ||
"publishId": "some-publish-id" | ||
} | ||
} | ||
} | ||
`); | ||
const pushEvent = makeBeamsPushEvent({}); | ||
@@ -303,9 +286,299 @@ // And an onNotificationReceived had been set | ||
const makePushEvent = payload => ({ | ||
waitUntil: () => {}, | ||
data: { | ||
json: () => JSON.parse(payload), | ||
}, | ||
test('SW should send delivery event when notification arrives', () => { | ||
jest.resetModules(); | ||
const devicestatestore = require('./device-state-store'); | ||
devicestatestore.default = makeDeviceStateStore({ | ||
deviceId: 'web-1db66b8a-f51f-49de-b225-72591535c855', | ||
token: 'some-token', | ||
userId: 'alice', | ||
}); | ||
const dorequest = require('./do-request'); | ||
const mockDoRequest = jest.fn(); | ||
mockDoRequest.mockReturnValueOnce(Promise.resolve('ok')); | ||
dorequest.default = mockDoRequest; | ||
require('./service-worker.js'); | ||
// Given a push event that comes from Pusher | ||
const pushEvent = makeBeamsPushEvent({}); | ||
// When the push listener is called | ||
const pushListener = listeners['push']; | ||
if (!pushListener) { | ||
throw new Error('No push listener has been set'); | ||
} | ||
pushListener(pushEvent); | ||
// Then the correct delivery event should be reported | ||
return new Promise(resolve => setTimeout(resolve, 200)).then(() => { | ||
expect(mockDoRequest.mock.calls.length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0].length).toBe(1); | ||
const requestOptions = mockDoRequest.mock.calls[0][0]; | ||
expect(requestOptions.method).toBe('POST'); | ||
expect(requestOptions.path).toBe( | ||
[ | ||
`https://${TEST_INSTANCE_ID}.pushnotifications.pusher.com`, | ||
`/reporting_api/v2/instances/${TEST_INSTANCE_ID}/events`, | ||
].join('') | ||
); | ||
expect(requestOptions.body.publishId).toBe(TEST_PUBLISH_ID); | ||
expect(requestOptions.body.event).toBe('delivery'); | ||
expect(requestOptions.body.userId).toBe('alice'); | ||
expect(requestOptions.body.timestampSecs).toBe(946684800); | ||
expect(requestOptions.body.appInBackground).toBe(true); | ||
expect(requestOptions.body.hasDisplayableContent).toBe(true); | ||
expect(requestOptions.body.hasData).toBe(false); | ||
}); | ||
}); | ||
test('SW should send open event when notification clicked', () => { | ||
jest.resetModules(); | ||
const devicestatestore = require('./device-state-store'); | ||
devicestatestore.default = makeDeviceStateStore({ | ||
deviceId: 'web-1db66b8a-f51f-49de-b225-72591535c855', | ||
token: 'some-token', | ||
userId: 'alice', | ||
}); | ||
const dorequest = require('./do-request'); | ||
const mockDoRequest = jest.fn(); | ||
mockDoRequest.mockReturnValueOnce(Promise.resolve('ok')); | ||
dorequest.default = mockDoRequest; | ||
require('./service-worker.js'); | ||
// Given a notification click event with a deep link | ||
const clickEvent = makeClickEvent({ | ||
data: { | ||
pusherPayload: { | ||
notification: { | ||
title: 'Hi!', | ||
body: 'This is a notification!', | ||
deep_link: 'https://pusher.com', | ||
}, | ||
data: { | ||
pusher: { | ||
instanceId: TEST_INSTANCE_ID, | ||
publishId: TEST_PUBLISH_ID, | ||
hasDisplayableContent: true, | ||
hasData: false, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}); | ||
// When the notificationclick listener is called | ||
const clickListener = listeners['notificationclick']; | ||
if (!clickListener) { | ||
throw new Error('No click listener has been set'); | ||
} | ||
clickListener(clickEvent); | ||
// Then an open event should be reported | ||
return new Promise(resolve => setTimeout(resolve, 200)).then(() => { | ||
expect(mockDoRequest.mock.calls.length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0].length).toBe(1); | ||
const requestOptions = mockDoRequest.mock.calls[0][0]; | ||
expect(requestOptions.method).toBe('POST'); | ||
expect(requestOptions.path).toBe( | ||
[ | ||
`https://${TEST_INSTANCE_ID}.pushnotifications.pusher.com`, | ||
`/reporting_api/v2/instances/${TEST_INSTANCE_ID}/events`, | ||
].join('') | ||
); | ||
expect(requestOptions.body.publishId).toBe(TEST_PUBLISH_ID); | ||
expect(requestOptions.body.event).toBe('open'); | ||
expect(requestOptions.body.userId).toBe('alice'); | ||
expect(requestOptions.body.timestampSecs).toBe(946684800); | ||
expect(requestOptions.body.appInBackground).toBe(true); | ||
expect(requestOptions.body.hasDisplayableContent).toBe(true); | ||
expect(requestOptions.body.hasData).toBe(false); | ||
}); | ||
}); | ||
test('SW should send event with appInBackground false given a visible client', () => { | ||
jest.resetModules(); | ||
const devicestatestore = require('./device-state-store'); | ||
devicestatestore.default = makeDeviceStateStore({ | ||
deviceId: 'web-1db66b8a-f51f-49de-b225-72591535c855', | ||
token: 'some-token', | ||
userId: 'alice', | ||
}); | ||
const dorequest = require('./do-request'); | ||
const mockDoRequest = jest.fn(); | ||
mockDoRequest.mockReturnValueOnce(Promise.resolve('ok')); | ||
dorequest.default = mockDoRequest; | ||
require('./service-worker.js'); | ||
// Given a push event that comes from Pusher | ||
const pushEvent = makeBeamsPushEvent({}); | ||
// and at least once visible client | ||
registerVisibleClient(); | ||
// When the push listener is called | ||
const pushListener = listeners['push']; | ||
if (!pushListener) { | ||
throw new Error('No push listener has been set'); | ||
} | ||
pushListener(pushEvent); | ||
// Then the correct delivery event should be reported | ||
return new Promise(resolve => setTimeout(resolve, 200)).then(() => { | ||
expect(mockDoRequest.mock.calls.length).toBe(1); | ||
expect(mockDoRequest.mock.calls[0].length).toBe(1); | ||
const requestOptions = mockDoRequest.mock.calls[0][0]; | ||
expect(requestOptions.body.appInBackground).toBe(false); | ||
}); | ||
}); | ||
test('SW should show notification if site has focus but hide flag is not set', () => { | ||
require('./service-worker.js'); | ||
// Given a push event that comes from Pusher without the flag set | ||
const pushEvent = makeBeamsPushEvent({ | ||
hide_notification_if_site_has_focus: undefined, | ||
}); | ||
// and at least once focused client | ||
registerFocusedClient(); | ||
// When the push listener is called | ||
const pushListener = listeners['push']; | ||
if (!pushListener) { | ||
throw new Error('No push listener has been set'); | ||
} | ||
pushListener(pushEvent); | ||
return pushEvent.getWaitUntilPromise().then(() => { | ||
// Then a notification should be shown | ||
expect(shownNotifications).toHaveLength(1); | ||
}); | ||
}); | ||
test('SW should show notification if site has focus but hide flag is false', () => { | ||
require('./service-worker.js'); | ||
// Given a push event that comes from Pusher with the flag set to false | ||
const pushEvent = makeBeamsPushEvent({ | ||
hide_notification_if_site_has_focus: false, | ||
}); | ||
// and at least once focused client | ||
registerFocusedClient(); | ||
// When the push listener is called | ||
const pushListener = listeners['push']; | ||
if (!pushListener) { | ||
throw new Error('No push listener has been set'); | ||
} | ||
pushListener(pushEvent); | ||
return pushEvent.getWaitUntilPromise().then(() => { | ||
// Then a notification should be shown | ||
expect(shownNotifications).toHaveLength(1); | ||
}); | ||
}); | ||
test('SW should not show notification if site has focus and hide flag is true', () => { | ||
require('./service-worker.js'); | ||
// Given a push event that comes from Pusher with the flag set | ||
const pushEvent = makeBeamsPushEvent({ | ||
hide_notification_if_site_has_focus: true, | ||
}); | ||
// and at least once focused client | ||
registerFocusedClient(); | ||
// When the push listener is called | ||
const pushListener = listeners['push']; | ||
if (!pushListener) { | ||
throw new Error('No push listener has been set'); | ||
} | ||
pushListener(pushEvent); | ||
return pushEvent.getWaitUntilPromise().then(() => { | ||
// Then a notification should not be shown | ||
expect(shownNotifications).toHaveLength(0); | ||
}); | ||
}); | ||
test('SW should show notification if site does not have focus and hide flag is true', () => { | ||
require('./service-worker.js'); | ||
// Given a push event that comes from Pusher with the flag set | ||
const pushEvent = makeBeamsPushEvent({ | ||
hide_notification_if_site_has_focus: true, | ||
}); | ||
// and no focused clients | ||
expect(clients).toHaveLength(0); | ||
// When the push listener is called | ||
const pushListener = listeners['push']; | ||
if (!pushListener) { | ||
throw new Error('No push listener has been set'); | ||
} | ||
pushListener(pushEvent); | ||
return pushEvent.getWaitUntilPromise().then(() => { | ||
// Then a notification should be shown | ||
expect(shownNotifications).toHaveLength(1); | ||
}); | ||
}); | ||
class FakePushEvent { | ||
constructor(payload) { | ||
this.data = { | ||
json: () => JSON.parse(payload), | ||
}; | ||
this.waitUntil = promise => { | ||
this.waitUntilPromise = promise; | ||
}; | ||
} | ||
getWaitUntilPromise() { | ||
expect(this.waitUntilPromise).not.toBeUndefined(); | ||
return this.waitUntilPromise; | ||
} | ||
} | ||
const makePushEvent = payload => new FakePushEvent(payload); | ||
const makeBeamsPushEvent = ({ | ||
instanceId = TEST_INSTANCE_ID, | ||
publishId = TEST_PUBLISH_ID, | ||
title = TEST_NOTIFICATION_TITLE, | ||
body = TEST_NOTIFICATION_BODY, | ||
icon = TEST_NOTIFICATION_ICON, | ||
hide_notification_if_site_has_focus = undefined, | ||
}) => | ||
makePushEvent( | ||
JSON.stringify({ | ||
notification: { title, body, icon, hide_notification_if_site_has_focus }, | ||
data: { | ||
pusher: { | ||
instanceId, | ||
publishId, | ||
hasDisplayableContent: true, | ||
hasData: false, | ||
}, | ||
}, | ||
}) | ||
); | ||
const makeClickEvent = ({ data }) => { | ||
@@ -326,1 +599,6 @@ let isOpen = true; | ||
}; | ||
const registerVisibleClient = () => | ||
clients.push({ visibilityState: 'visible' }); | ||
const registerFocusedClient = () => clients.push({ focused: true }); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
319154
38
8583
1
28
11