PairPush.js - viames/pair GitHub Wiki
/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.
Place the file in public assets and include it in your layout:
<script src="/assets/PairPush.js"></script>Global object:
window.PairPush- HTTPS (or localhost in development)
- a valid service worker (
/sw.jsor custom path) - a VAPID public key
- backend endpoints for storing/removing subscriptions
-
NotificationAPI availability if you usegetPermission()/requestPermission()
Default backend endpoints:
POST /push/subscribePOST /push/unsubscribe
<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>Checks if required browser APIs are available:
navigator.serviceWorkerwindow.PushManager
Example:
if (!PairPush.isSupported()) {
// hide push toggle UI
document.querySelector('[data-enable-push]')?.setAttribute('hidden', 'hidden');
}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);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');
}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:
- register service worker (
swUrl) - convert VAPID key to
Uint8Array - call
registration.pushManager.subscribe(...) -
POST subscribeUrlwith{ subscription } - return
PushSubscription
Errors:
- throws if unsupported browser
- throws if
vapidPublicKeyis missing - throws for browser or network errors
- does not throw on non-2xx from
subscribeUrl(only network failures rejectfetch)
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:
- register service worker (
swUrl) - read current subscription
- return
falseif no active subscription -
POST unsubscribeUrlwith{ endpoint } - 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);<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>{
"subscription": {
"endpoint": "https://push-service/...",
"keys": {
"p256dh": "...",
"auth": "..."
}
}
}{
"endpoint": "https://push-service/..."
}- 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
swUrlacrossPairPushandPairPWA - expose clear UX for denied permissions