PairPush.js - viames/pair GitHub Wiki

Pair framework: PairPush.js

/assets/PairPush.js is the browser helper for Web Push subscription lifecycle in Pair applications.

It focuses on:

  • capability checks
  • service worker registration
  • permission handling
  • subscription + backend sync
  • unsubscription + backend cleanup

PairPush is intentionally simple: it does not try to implement a full notification framework.

Installation

Place the file in public assets and include it in your layout:

<script src="/assets/PairPush.js"></script>

Global object:

window.PairPush

Requirements

  • HTTPS (or localhost in development)
  • a valid service worker (/sw.js or custom path)
  • a VAPID public key
  • backend endpoints for storing/removing subscriptions
  • Notification API availability if you use getPermission() / requestPermission()

Default backend endpoints:

  • POST /push/subscribe
  • POST /push/unsubscribe

Quick start

<button type="button" id="enablePush">Enable notifications</button>

<script>
const button = document.getElementById('enablePush');

button.addEventListener('click', async () => {
  if (!PairPush.isSupported()) {
    alert('Web Push not supported in this browser');
    return;
  }

  if (typeof Notification === 'undefined') {
    alert('Notification API not available in this browser');
    return;
  }

  try {
    await PairPush.registerServiceWorker('/sw.js');

    const permission = await PairPush.requestPermission();
    if (permission !== 'granted') {
      console.log('Permission not granted:', permission);
      return;
    }

    const subscription = await PairPush.subscribe({
      vapidPublicKey: window.APP_VAPID_PUBLIC_KEY,
      subscribeUrl: '/push/subscribe',
      swUrl: '/sw.js'
    });

    console.log('Push enabled:', subscription.endpoint);
  } catch (error) {
    console.error('Unable to enable push', error);
  }
});
</script>

API reference

PairPush.isSupported(): boolean

Checks if required browser APIs are available:

  • navigator.serviceWorker
  • window.PushManager

Example:

if (!PairPush.isSupported()) {
  // hide push toggle UI
  document.querySelector('[data-enable-push]')?.setAttribute('hidden', 'hidden');
}

PairPush.registerServiceWorker(swUrl = '/sw.js'): Promise<ServiceWorkerRegistration>

Registers the service worker used for push subscription.

  • throws if push is not supported

Example:

const registration = await PairPush.registerServiceWorker('/sw.js');
console.log('SW scope:', registration.scope);

PairPush.getPermission(): Promise<string>

Returns current notification permission (default, granted, denied).

Note: PairPush.isSupported() checks serviceWorker and PushManager, but not Notification. If you target constrained/non-standard environments, guard it explicitly.

Example:

if (typeof Notification === 'undefined') {
  console.log('Notification API unavailable');
  return;
}

const permission = await PairPush.getPermission();
if (permission === 'denied') {
  console.log('User blocked notifications at browser level');
}

PairPush.requestPermission(): Promise<string>

Requests notification permission from the user.

Use after a user gesture (button click).

document.getElementById('askPermission').addEventListener('click', async () => {
  const permission = await PairPush.requestPermission();
  console.log('Permission:', permission);
});

PairPush.subscribe({ vapidPublicKey, subscribeUrl = '/push/subscribe', swUrl = '/sw.js' }): Promise<PushSubscription>

Flow:

  1. register service worker (swUrl)
  2. convert VAPID key to Uint8Array
  3. call registration.pushManager.subscribe(...)
  4. POST subscribeUrl with { subscription }
  5. return PushSubscription

Errors:

  • throws if unsupported browser
  • throws if vapidPublicKey is missing
  • throws for browser or network errors
  • does not throw on non-2xx from subscribeUrl (only network failures reject fetch)

Example with custom endpoints:

const subscription = await PairPush.subscribe({
  vapidPublicKey: window.APP_VAPID_PUBLIC_KEY,
  subscribeUrl: '/api/push/subscribe',
  swUrl: '/assets/sw.js'
});

If you need strict backend confirmation, add an explicit verification request:

await PairPush.subscribe({
  vapidPublicKey: window.APP_VAPID_PUBLIC_KEY,
  subscribeUrl: '/api/push/subscribe',
  swUrl: '/assets/sw.js'
});

const verify = await fetch('/api/push/subscription/status', { credentials: 'same-origin' });
if (!verify.ok) {
  throw new Error('Subscription was created in browser but not confirmed by backend');
}

PairPush.unsubscribe({ unsubscribeUrl = '/push/unsubscribe', swUrl = '/sw.js' } = {}): Promise<boolean>

Flow:

  1. register service worker (swUrl)
  2. read current subscription
  3. return false if no active subscription
  4. POST unsubscribeUrl with { endpoint }
  5. call subscription.unsubscribe()

Important: non-2xx from unsubscribeUrl does not automatically reject unless the request fails at network level.

Example:

const removed = await PairPush.unsubscribe({
  unsubscribeUrl: '/api/push/unsubscribe',
  swUrl: '/assets/sw.js'
});

console.log('Unsubscribe result:', removed);

UI pattern: enable/disable toggle

<label>
  <input type="checkbox" id="pushToggle" />
  Enable notifications
</label>

<script>
const toggle = document.getElementById('pushToggle');

async function syncToggle() {
  if (!PairPush.isSupported()) {
    toggle.disabled = true;
    return;
  }

  if (typeof Notification === 'undefined') {
    toggle.disabled = true;
    return;
  }

  const permission = await PairPush.getPermission();
  if (permission === 'denied') {
    toggle.disabled = true;
    return;
  }

  const reg = await PairPush.registerServiceWorker('/sw.js');
  const existing = await reg.pushManager.getSubscription();
  toggle.checked = !!existing;
}

async function onToggleChange() {
  try {
    if (typeof Notification === 'undefined') {
      toggle.checked = false;
      toggle.disabled = true;
      return;
    }

    if (toggle.checked) {
      const permission = await PairPush.requestPermission();
      if (permission !== 'granted') {
        toggle.checked = false;
        return;
      }

      await PairPush.subscribe({
        vapidPublicKey: window.APP_VAPID_PUBLIC_KEY,
        subscribeUrl: '/push/subscribe',
        swUrl: '/sw.js'
      });
    } else {
      await PairPush.unsubscribe({
        unsubscribeUrl: '/push/unsubscribe',
        swUrl: '/sw.js'
      });
    }
  } catch (error) {
    console.error(error);
    toggle.checked = !toggle.checked;
  }
}

toggle.addEventListener('change', onToggleChange);
syncToggle();
</script>

Backend payload contracts

Subscribe endpoint payload

{
  "subscription": {
    "endpoint": "https://push-service/...",
    "keys": {
      "p256dh": "...",
      "auth": "..."
    }
  }
}

Unsubscribe endpoint payload

{
  "endpoint": "https://push-service/..."
}

Best practices

  • ask permission only on explicit user action
  • treat subscribe/unsubscribe endpoints as idempotent
  • remove stale endpoints when push provider returns 404/410
  • keep one consistent swUrl across PairPush and PairPWA
  • expose clear UX for denied permissions

Related pages

⚠️ **GitHub.com Fallback** ⚠️