@ -1,57 +0,0 @@ | |||
import axios from 'axios'; | |||
import { pushNotificationsSetting } from '../settings'; | |||
export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT'; | |||
export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION'; | |||
export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION'; | |||
export const ALERTS_CHANGE = 'PUSH_NOTIFICATIONS_ALERTS_CHANGE'; | |||
export function setBrowserSupport (value) { | |||
return { | |||
type: SET_BROWSER_SUPPORT, | |||
value, | |||
}; | |||
} | |||
export function setSubscription (subscription) { | |||
return { | |||
type: SET_SUBSCRIPTION, | |||
subscription, | |||
}; | |||
} | |||
export function clearSubscription () { | |||
return { | |||
type: CLEAR_SUBSCRIPTION, | |||
}; | |||
} | |||
export function changeAlerts(key, value) { | |||
return dispatch => { | |||
dispatch({ | |||
type: ALERTS_CHANGE, | |||
key, | |||
value, | |||
}); | |||
dispatch(saveSettings()); | |||
}; | |||
} | |||
export function saveSettings() { | |||
return (_, getState) => { | |||
const state = getState().get('push_notifications'); | |||
const subscription = state.get('subscription'); | |||
const alerts = state.get('alerts'); | |||
const data = { alerts }; | |||
axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, { | |||
data, | |||
}).then(() => { | |||
const me = getState().getIn(['meta', 'me']); | |||
if (me) { | |||
pushNotificationsSetting.set(me, data); | |||
} | |||
}); | |||
}; | |||
} |
@ -0,0 +1,23 @@ | |||
import { | |||
SET_BROWSER_SUPPORT, | |||
SET_SUBSCRIPTION, | |||
CLEAR_SUBSCRIPTION, | |||
SET_ALERTS, | |||
setAlerts, | |||
} from './setter'; | |||
import { register, saveSettings } from './registerer'; | |||
export { | |||
SET_BROWSER_SUPPORT, | |||
SET_SUBSCRIPTION, | |||
CLEAR_SUBSCRIPTION, | |||
SET_ALERTS, | |||
register, | |||
}; | |||
export function changeAlerts(key, value) { | |||
return dispatch => { | |||
dispatch(setAlerts(key, value)); | |||
dispatch(saveSettings()); | |||
}; | |||
} |
@ -0,0 +1,149 @@ | |||
import axios from 'axios'; | |||
import { pushNotificationsSetting } from '../../settings'; | |||
import { setBrowserSupport, setSubscription, clearSubscription } from './setter'; | |||
// Taken from https://www.npmjs.com/package/web-push | |||
const urlBase64ToUint8Array = (base64String) => { | |||
const padding = '='.repeat((4 - base64String.length % 4) % 4); | |||
const base64 = (base64String + padding) | |||
.replace(/\-/g, '+') | |||
.replace(/_/g, '/'); | |||
const rawData = window.atob(base64); | |||
const outputArray = new Uint8Array(rawData.length); | |||
for (let i = 0; i < rawData.length; ++i) { | |||
outputArray[i] = rawData.charCodeAt(i); | |||
} | |||
return outputArray; | |||
}; | |||
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content'); | |||
const getRegistration = () => navigator.serviceWorker.ready; | |||
const getPushSubscription = (registration) => | |||
registration.pushManager.getSubscription() | |||
.then(subscription => ({ registration, subscription })); | |||
const subscribe = (registration) => | |||
registration.pushManager.subscribe({ | |||
userVisibleOnly: true, | |||
applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()), | |||
}); | |||
const unsubscribe = ({ registration, subscription }) => | |||
subscription ? subscription.unsubscribe().then(() => registration) : registration; | |||
const sendSubscriptionToBackend = (subscription, me) => { | |||
const params = { subscription }; | |||
if (me) { | |||
const data = pushNotificationsSetting.get(me); | |||
if (data) { | |||
params.data = data; | |||
} | |||
} | |||
return axios.post('/api/web/push_subscriptions', params).then(response => response.data); | |||
}; | |||
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload | |||
const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype); | |||
export default function register () { | |||
return (dispatch, getState) => { | |||
dispatch(setBrowserSupport(supportsPushNotifications)); | |||
const me = getState().getIn(['meta', 'me']); | |||
if (me && !pushNotificationsSetting.get(me)) { | |||
const alerts = getState().getIn(['push_notifications', 'alerts']); | |||
if (alerts) { | |||
pushNotificationsSetting.set(me, { alerts: alerts }); | |||
} | |||
} | |||
if (supportsPushNotifications) { | |||
if (!getApplicationServerKey()) { | |||
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.'); | |||
return; | |||
} | |||
getRegistration() | |||
.then(getPushSubscription) | |||
.then(({ registration, subscription }) => { | |||
if (subscription !== null) { | |||
// We have a subscription, check if it is still valid | |||
const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString(); | |||
const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString(); | |||
const serverEndpoint = getState().getIn(['push_notifications', 'subscription', 'endpoint']); | |||
// If the VAPID public key did not change and the endpoint corresponds | |||
// to the endpoint saved in the backend, the subscription is valid | |||
if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) { | |||
return subscription; | |||
} else { | |||
// Something went wrong, try to subscribe again | |||
return unsubscribe({ registration, subscription }).then(subscribe).then( | |||
subscription => sendSubscriptionToBackend(subscription, me)); | |||
} | |||
} | |||
// No subscription, try to subscribe | |||
return subscribe(registration).then( | |||
subscription => sendSubscriptionToBackend(subscription, me)); | |||
}) | |||
.then(subscription => { | |||
// If we got a PushSubscription (and not a subscription object from the backend) | |||
// it means that the backend subscription is valid (and was set during hydration) | |||
if (!(subscription instanceof PushSubscription)) { | |||
dispatch(setSubscription(subscription)); | |||
if (me) { | |||
pushNotificationsSetting.set(me, { alerts: subscription.alerts }); | |||
} | |||
} | |||
}) | |||
.catch(error => { | |||
if (error.code === 20 && error.name === 'AbortError') { | |||
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.'); | |||
} else if (error.code === 5 && error.name === 'InvalidCharacterError') { | |||
console.error('The VAPID public key seems to be invalid:', getApplicationServerKey()); | |||
} | |||
// Clear alerts and hide UI settings | |||
dispatch(clearSubscription()); | |||
if (me) { | |||
pushNotificationsSetting.remove(me); | |||
} | |||
try { | |||
getRegistration() | |||
.then(getPushSubscription) | |||
.then(unsubscribe); | |||
} catch (e) { | |||
} | |||
}); | |||
} else { | |||
console.warn('Your browser does not support Web Push Notifications.'); | |||
} | |||
}; | |||
} | |||
export function saveSettings() { | |||
return (_, getState) => { | |||
const state = getState().get('push_notifications'); | |||
const subscription = state.get('subscription'); | |||
const alerts = state.get('alerts'); | |||
const data = { alerts }; | |||
axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, { | |||
data, | |||
}).then(() => { | |||
const me = getState().getIn(['meta', 'me']); | |||
if (me) { | |||
pushNotificationsSetting.set(me, data); | |||
} | |||
}); | |||
}; | |||
} |
@ -0,0 +1,34 @@ | |||
export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT'; | |||
export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION'; | |||
export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION'; | |||
export const SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS'; | |||
export function setBrowserSupport (value) { | |||
return { | |||
type: SET_BROWSER_SUPPORT, | |||
value, | |||
}; | |||
} | |||
export function setSubscription (subscription) { | |||
return { | |||
type: SET_SUBSCRIPTION, | |||
subscription, | |||
}; | |||
} | |||
export function clearSubscription () { | |||
return { | |||
type: CLEAR_SUBSCRIPTION, | |||
}; | |||
} | |||
export function setAlerts (key, value) { | |||
return dispatch => { | |||
dispatch({ | |||
type: SET_ALERTS, | |||
key, | |||
value, | |||
}); | |||
}; | |||
} |
@ -1,129 +0,0 @@ | |||
import axios from 'axios'; | |||
import { store } from './containers/mastodon'; | |||
import { setBrowserSupport, setSubscription, clearSubscription } from './actions/push_notifications'; | |||
import { pushNotificationsSetting } from './settings'; | |||
// Taken from https://www.npmjs.com/package/web-push | |||
const urlBase64ToUint8Array = (base64String) => { | |||
const padding = '='.repeat((4 - base64String.length % 4) % 4); | |||
const base64 = (base64String + padding) | |||
.replace(/\-/g, '+') | |||
.replace(/_/g, '/'); | |||
const rawData = window.atob(base64); | |||
const outputArray = new Uint8Array(rawData.length); | |||
for (let i = 0; i < rawData.length; ++i) { | |||
outputArray[i] = rawData.charCodeAt(i); | |||
} | |||
return outputArray; | |||
}; | |||
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content'); | |||
const getRegistration = () => navigator.serviceWorker.ready; | |||
const getPushSubscription = (registration) => | |||
registration.pushManager.getSubscription() | |||
.then(subscription => ({ registration, subscription })); | |||
const subscribe = (registration) => | |||
registration.pushManager.subscribe({ | |||
userVisibleOnly: true, | |||
applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()), | |||
}); | |||
const unsubscribe = ({ registration, subscription }) => | |||
subscription ? subscription.unsubscribe().then(() => registration) : registration; | |||
const sendSubscriptionToBackend = (subscription) => { | |||
const params = { subscription }; | |||
const me = store.getState().getIn(['meta', 'me']); | |||
if (me) { | |||
const data = pushNotificationsSetting.get(me); | |||
if (data) { | |||
params.data = data; | |||
} | |||
} | |||
return axios.post('/api/web/push_subscriptions', params).then(response => response.data); | |||
}; | |||
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload | |||
const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype); | |||
export function register () { | |||
store.dispatch(setBrowserSupport(supportsPushNotifications)); | |||
const me = store.getState().getIn(['meta', 'me']); | |||
if (me && !pushNotificationsSetting.get(me)) { | |||
const alerts = store.getState().getIn(['push_notifications', 'alerts']); | |||
if (alerts) { | |||
pushNotificationsSetting.set(me, { alerts: alerts }); | |||
} | |||
} | |||
if (supportsPushNotifications) { | |||
if (!getApplicationServerKey()) { | |||
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.'); | |||
return; | |||
} | |||
getRegistration() | |||
.then(getPushSubscription) | |||
.then(({ registration, subscription }) => { | |||
if (subscription !== null) { | |||
// We have a subscription, check if it is still valid | |||
const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString(); | |||
const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString(); | |||
const serverEndpoint = store.getState().getIn(['push_notifications', 'subscription', 'endpoint']); | |||
// If the VAPID public key did not change and the endpoint corresponds | |||
// to the endpoint saved in the backend, the subscription is valid | |||
if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) { | |||
return subscription; | |||
} else { | |||
// Something went wrong, try to subscribe again | |||
return unsubscribe({ registration, subscription }).then(subscribe).then(sendSubscriptionToBackend); | |||
} | |||
} | |||
// No subscription, try to subscribe | |||
return subscribe(registration).then(sendSubscriptionToBackend); | |||
}) | |||
.then(subscription => { | |||
// If we got a PushSubscription (and not a subscription object from the backend) | |||
// it means that the backend subscription is valid (and was set during hydration) | |||
if (!(subscription instanceof PushSubscription)) { | |||
store.dispatch(setSubscription(subscription)); | |||
if (me) { | |||
pushNotificationsSetting.set(me, { alerts: subscription.alerts }); | |||
} | |||
} | |||
}) | |||
.catch(error => { | |||
if (error.code === 20 && error.name === 'AbortError') { | |||
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.'); | |||
} else if (error.code === 5 && error.name === 'InvalidCharacterError') { | |||
console.error('The VAPID public key seems to be invalid:', getApplicationServerKey()); | |||
} | |||
// Clear alerts and hide UI settings | |||
store.dispatch(clearSubscription()); | |||
if (me) { | |||
pushNotificationsSetting.remove(me); | |||
} | |||
try { | |||
getRegistration() | |||
.then(getPushSubscription) | |||
.then(unsubscribe); | |||
} catch (e) { | |||
} | |||
}); | |||
} else { | |||
console.warn('Your browser does not support Web Push Notifications.'); | |||
} | |||
} |