about summary refs log tree commit diff
path: root/app/javascript/mastodon/actions/push_notifications/registerer.js
blob: b0f42b6a20e3091c1c3a7fde321c47d2a457d6b9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import api from '../../api';
import { decode as decodeBase64 } from '../../utils/base64';
import { pushNotificationsSetting } from '../../settings';
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
import { me } from '../../initial_state';

// 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, '/');

  return decodeBase64(base64);
};

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 };

  if (me) {
    const data = pushNotificationsSetting.get(me);
    if (data) {
      params.data = data;
    }
  }

  return api().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 () {
  return (dispatch, getState) => {
    dispatch(setBrowserSupport(supportsPushNotifications));

    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));
            }
          }

          // No subscription, try to subscribe
          return subscribe(registration).then(
            subscription => sendSubscriptionToBackend(subscription));
        })
        .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);
          }

          return getRegistration()
            .then(getPushSubscription)
            .then(unsubscribe);
        })
        .catch(console.warn);
    } 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 };

    api().put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
      data,
    }).then(() => {
      if (me) {
        pushNotificationsSetting.set(me, data);
      }
    }).catch(console.warn);
  };
}