@visionpush/web-sdk
v1.0.1
Published
VisionPush Web Push SDK — VAPID-based push notifications with Safari support
Downloads
335
Maintainers
Readme
@visionpush/web-sdk
VisionPush Web Push SDK for browsers — TypeScript-first, RFC 8030 compliant, with full Safari Web Push support (macOS 13+ / iOS 16.4+) and a first-class React/Next.js integration.
Features
- TypeScript-first — complete type definitions, strict mode, no
any - RFC 8030 + VAPID — standard Web Push with application server authentication
- Safari Web Push — works natively on Safari 16.4+ (macOS 13, iOS 16.4) without any special casing
- Service Worker managed — background push handling, notification display, click tracking
- Permission UX best practices — never prompt on page load, detect sticky "denied" state, track dismissals
- Analytics — track notification delivered / clicked / dismissed events
- React hook —
useVisionPush()with reactive state and stable callbacks - Next.js App Router compatible — SSR-safe, no
windowaccess on the server - ESM + CJS builds — works in all modern bundlers (Vite, webpack, esbuild, Rollup)
- Clean architecture — five focused classes with clear boundaries
Installation
npm install @visionpush/web-sdk
# or
pnpm add @visionpush/web-sdk
# or
yarn add @visionpush/web-sdkService Worker Setup
Copy the service worker script to your web root:
cp node_modules/@visionpush/web-sdk/public/service-worker.js public/visionpush-sw.jsThe file must be served from the same origin as your app and at the path you configure in serviceWorkerUrl (default: /visionpush-sw.js).
Existing service worker? Import the VisionPush worker from yours:
// your-sw.js
importScripts('/visionpush-sw.js');
// … your own logic belowQuick Start
Vanilla JS / TypeScript
import { VisionPushClient } from '@visionpush/web-sdk';
const vp = new VisionPushClient({
appId: 'your-app-uuid',
apiKey: 'vp_live_xxxxxxxxxxxxxxxxxxxxxxxx',
vapidPublicKey: 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
});
// Initialize once when the app loads.
await vp.initialize();
// Request permission from a user gesture (e.g., button click).
document.getElementById('subscribe-btn').addEventListener('click', async () => {
try {
const result = await vp.requestPermission();
if (result.granted) {
console.log('Subscribed! Subscriber ID:', vp.getSubscriberId());
}
} catch (err) {
if (err.code === 'PERMISSION_DENIED') {
showPermissionDeniedBanner();
}
}
});
// React to incoming notifications (foreground).
vp.on('notification:clicked', (event) => {
window.open(event.payload.url, '_blank');
});
vp.on('notification:received', (event) => {
showInAppNotificationToast(event.payload);
});React
import { useVisionPush } from '@visionpush/web-sdk/react';
export function NotificationBell() {
const { isPermitted, isSubscribed, requestPermission, error, isReady } = useVisionPush({
appId: process.env.NEXT_PUBLIC_VP_APP_ID!,
apiKey: process.env.NEXT_PUBLIC_VP_API_KEY!,
vapidPublicKey: process.env.NEXT_PUBLIC_VP_VAPID_KEY!,
onNotification: (event) => {
if (event.type === 'notification:clicked') {
router.push(event.payload.url ?? '/notifications');
}
},
});
if (!isReady) return null;
return (
<button
onClick={requestPermission}
disabled={isSubscribed}
aria-label={isSubscribed ? 'Notifications enabled' : 'Enable notifications'}
>
{isSubscribed ? '🔔 Subscribed' : '🔕 Enable Notifications'}
{error && <span className="error">{error.message}</span>}
</button>
);
}Next.js App Router
Wrap the hook in a client component (the SDK uses browser APIs):
// app/components/PushProvider.tsx
'use client';
import { useVisionPush } from '@visionpush/web-sdk/react';
export function PushProvider({ children }: { children: React.ReactNode }) {
useVisionPush({
appId: process.env.NEXT_PUBLIC_VP_APP_ID!,
apiKey: process.env.NEXT_PUBLIC_VP_API_KEY!,
vapidPublicKey: process.env.NEXT_PUBLIC_VP_VAPID_KEY!,
autoSubscribeIfGranted: true,
});
return <>{children}</>;
}
// app/layout.tsx
import { PushProvider } from './components/PushProvider';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<PushProvider>{children}</PushProvider>
</body>
</html>
);
}Configuration
VisionPushClientOptions
| Option | Type | Default | Description |
|---|---|---|---|
| appId | string | required | UUID of your VisionPush application |
| apiKey | string | required | Public API key (prefix vp_live_ or vp_test_) |
| vapidPublicKey | string | — | VAPID public key (URL-safe Base64). Fetched from API if omitted. |
| apiBaseUrl | string | https://api.visionpush.de/v1 | Override for staging or self-hosted deployments |
| serviceWorkerUrl | string | /visionpush-sw.js | Path to the service worker script |
| serviceWorkerScope | string | / | Service worker scope |
| debug | boolean | false | Enable verbose console logging |
| maxRetries | number | 3 | Maximum API request retries |
| retryDelayMs | number | 1000 | Initial retry delay (ms); uses exponential back-off |
API Reference
VisionPushClient
Lifecycle
const vp = new VisionPushClient(options);
await vp.initialize(); // Must be called before any other method.
vp.destroy(); // Tear down listeners and cancel pending requests.Permission & Subscription
await vp.requestPermission(); // Shows browser permission prompt; subscribes on grant.
await vp.unsubscribe(); // Removes subscription from browser and backend.
vp.getPermissionStatus(); // 'default' | 'granted' | 'denied'
vp.isPermitted(); // true if granted
vp.isSubscribed(); // true if active push subscription exists
vp.getSubscriberId(); // VisionPush subscriber ID or null
vp.isReady(); // true after successful initialize()Events
// Subscribe to an event — returns an unsubscribe function.
const off = vp.on('notification:clicked', (event) => { ... });
off(); // Remove the listener.
// One-time listener.
vp.once('notification:received', (event) => { ... });
// Remove a specific listener.
vp.off('notification:clicked', myHandler);Available events:
| Event | Payload type | Description |
|---|---|---|
| notification | NotificationEvent | All notification events (union) |
| notification:received | NotificationReceivedEvent | Push message received while page is open |
| notification:clicked | NotificationClickedEvent | User clicked the notification |
| notification:dismissed | NotificationDismissedEvent | User dismissed without clicking |
| permission:changed | { status: PermissionStatus } | Browser permission status changed |
| subscription:created | PushSubscriptionData | New push subscription registered |
| subscription:deleted | { endpoint: string } | Subscription removed |
| error | VisionPushError | SDK error (non-fatal) |
useVisionPush(options) hook
| Return value | Type | Description |
|---|---|---|
| readyState | 'idle' \| 'initializing' \| 'ready' \| 'error' | SDK initialization state |
| isReady | boolean | Shorthand for readyState === 'ready' |
| permissionStatus | PermissionStatus | Current notification permission |
| isPermitted | boolean | Shorthand for permissionStatus === 'granted' |
| isSubscribed | boolean | Active push subscription exists |
| subscriberId | string \| null | VisionPush subscriber ID |
| error | VisionPushError \| null | Last SDK error |
| requestPermission | () => Promise<boolean> | Request permission + subscribe |
| unsubscribe | () => Promise<void> | Remove subscription |
| client | VisionPushClient \| null | Direct SDK access |
Additional options for the hook:
| Option | Type | Default | Description |
|---|---|---|---|
| autoSubscribeIfGranted | boolean | true | Re-subscribe silently if permission already granted |
| onNotification | (event) => void | — | Called for every notification event |
useVisionPushEvent(client, event, handler) hook
Advanced hook for subscribing to a specific SDK event within a component:
useVisionPushEvent(client, 'notification:clicked', (event) => {
router.push(event.payload.url ?? '/');
});Error Handling
All SDK errors extend VisionPushError and expose a code property for programmatic handling:
import {
VisionPushError,
PermissionDeniedError,
PermissionDismissedError,
SubscriptionError,
ApiError,
NetworkError,
} from '@visionpush/web-sdk';
try {
await vp.requestPermission();
} catch (err) {
if (err instanceof PermissionDeniedError) {
// User has explicitly denied — don't ask again.
showSettingsGuide();
} else if (err instanceof PermissionDismissedError) {
// Prompt was closed — we can try again later.
scheduleReminderAfterDelay();
} else if (err instanceof ApiError) {
console.error(`API error ${err.statusCode}:`, err.message);
} else if (err instanceof NetworkError) {
showOfflineBanner();
}
}Error codes:
| Code | Class | Description |
|---|---|---|
| NOT_INITIALIZED | NotInitializedError | Method called before initialize() |
| BROWSER_NOT_SUPPORTED | BrowserNotSupportedError | Browser lacks required APIs |
| SERVICE_WORKER_REGISTRATION_FAILED | ServiceWorkerRegistrationError | SW registration error |
| PERMISSION_DENIED | PermissionDeniedError | User denied notifications |
| PERMISSION_DISMISSED | PermissionDismissedError | User dismissed the prompt |
| SUBSCRIPTION_FAILED | SubscriptionError | Push subscription creation failed |
| API_ERROR | ApiError | VisionPush API returned an error |
| NETWORK_ERROR | NetworkError | Network failure |
| INVALID_CONFIG | InvalidConfigError | Bad constructor options |
Browser Support
| Browser | Version | Notes | |---|---|---| | Chrome | 50+ | Full support | | Edge | 79+ | Full support (Chromium-based) | | Firefox | 44+ | Full support | | Safari | 16.4+ | macOS 13 Ventura, iOS 16.4 — W3C Push API | | Samsung Internet | 5.0+ | Full support | | Opera | 37+ | Full support |
Architecture
VisionPushClient ← Main orchestrator; event emitter
├── ServiceWorkerManager ← SW registration, updates, messaging
├── PermissionManager ← Permission state, prompt flow, change watcher
├── PushSubscriptionManager ← VAPID subscribe/unsubscribe, cache
├── ApiClient ← Typed fetch wrapper with retry + auth
└── NotificationTracker ← Deduped event reportingThe service worker (public/service-worker.js) runs independently in the browser's SW thread and communicates with the SDK via BroadcastChannel (with a postMessage fallback for older Safari).
Publishing Checklist
- Copy
public/service-worker.jsto yourpublic/directory. - Add
NEXT_PUBLIC_VP_APP_ID,NEXT_PUBLIC_VP_API_KEY, andNEXT_PUBLIC_VP_VAPID_KEYto your environment. - Ensure the SW file is served with
Service-Worker-Allowedheader if the scope exceeds its directory. - Verify HTTPS — Web Push requires a secure context (or
localhost).
License
MIT © VisionLab
