@cloudsignal/pwa-sdk
v1.2.4
Published
CloudSignal PWA SDK - Progressive Web App features with push notifications, JWT/HMAC authentication, installation management, device tracking, offline queue, wake lock, and notification analytics
Maintainers
Readme
CloudSignal PWA SDK
Progressive Web App SDK for CloudSignal platform with push notifications, installation management, comprehensive device tracking, offline resilience, and notification analytics.
Features
Core Features
- PWA Installation - Automatic install prompt handling with iOS manual instructions
- Push Notifications - Web Push Protocol with VAPID authentication
- Device Detection - 35+ device/browser/OS detection fields
- Heartbeat - Real-time online status tracking
- Service Worker - Badge management, notification history, offline support
- TypeScript - Full type definitions included
v1.2.3 Features
- Auto Installation Tracking - Automatically registers PWA installations with backend when detected
registerInstallation()method for manual installation trackingisInstallationRegistered()/getInstallationId()helper methodsinstall:registeredevent for installation tracking
v1.2.0 Features
- JWT Authentication - User-linked registrations via JWT tokens (Supabase, Firebase, Auth0, Clerk)
- Dual Auth Mode - HMAC for anonymous users, JWT for authenticated users
- Smart Notification Routing - Enable in-app vs web push based on user online status
- Token Refresh - Automatic retry on 401 with
onTokenExpiredcallback
v1.1.0 Features
- Screen Wake Lock - Prevent screen sleep during critical operations
- Offline Request Queue - IndexedDB-backed queue with auto-retry when online
- iOS Install Banner - Beautiful modal guiding iOS Safari users to install
- Network-Aware Heartbeat - Auto-adjusts based on connection quality (4G/3G/2G)
- Battery-Aware Heartbeat - Pauses when battery <15% to save power
- Notification Analytics - Track displayed/clicked/dismissed events
Installation
npm
npm install @cloudsignal/pwa-sdkCDN
<!-- Latest version -->
<script src="https://cdn.cloudsignal.io/CloudSignalPWA.js"></script>
<!-- Specific version -->
<script src="https://cdn.cloudsignal.io/cloudsignal-pwa.v1.0.0.js"></script>Prerequisites
Environment Variables
Set up your environment variables before using the SDK:
# .env.local (Next.js) or .env
NEXT_PUBLIC_CLOUDSIGNAL_ORG_ID=your-org-uuid
NEXT_PUBLIC_CLOUDSIGNAL_SERVICE_ID=your-service-uuid
NEXT_PUBLIC_CLOUDSIGNAL_ORG_SECRET=your-secret-key # For HMAC mode onlyWhere to get these values:
- Organization ID - From CloudSignal dashboard → Organization Settings
- Service ID - From CloudSignal dashboard → PWA Services → Your Service
- Organization Secret - From CloudSignal dashboard → API Keys (for HMAC mode)
Quick Start
Basic Usage
import { CloudSignalPWA } from '@cloudsignal/pwa-sdk'
const pwa = new CloudSignalPWA({
organizationId: 'your-org-uuid',
organizationSecret: 'your-secret-key',
serviceId: 'your-service-uuid',
debug: true
})
// Initialize (downloads config, registers service worker)
await pwa.initialize()
// Get device information
const deviceInfo = pwa.getDeviceInfo()
console.log('Device:', deviceInfo.deviceModel, deviceInfo.browser)
// Check installation state - returns InstallationState object
const installState = pwa.getInstallationState()
if (installState.canBeInstalled && !installState.isInstalled) {
await pwa.showInstallPrompt()
}
// Register for push notifications
const registration = await pwa.registerForPush({
userEmail: '[email protected]'
})
// Access registration ID (note: registrationId, not id)
if (registration) {
console.log('Registered with ID:', registration.registrationId)
}CDN Usage
<script src="https://cdn.cloudsignal.io/CloudSignalPWA.js"></script>
<script>
const pwa = new CloudSignalPWA.CloudSignalPWA({
organizationId: 'your-org-uuid',
organizationSecret: 'your-secret-key',
serviceId: 'your-service-uuid'
})
pwa.initialize().then(() => {
console.log('PWA initialized')
})
</script>JWT Authentication (v1.2.0)
For authenticated user registrations, use JWT instead of HMAC:
import { CloudSignalPWA } from '@cloudsignal/pwa-sdk'
import { supabase } from './supabase' // Your Supabase client
const pwa = new CloudSignalPWA({
organizationId: 'your-org-uuid',
userToken: (await supabase.auth.getSession()).data.session?.access_token,
serviceId: 'your-service-uuid',
onTokenExpired: async () => {
// Refresh token when 401 received
const { data } = await supabase.auth.refreshSession()
return data.session?.access_token || ''
}
})
await pwa.initialize()
// Registration will be linked to the authenticated user
const registration = await pwa.registerForPush()Upgrading from Anonymous to Authenticated:
// Start with HMAC (anonymous)
const pwa = new CloudSignalPWA({
organizationId: 'your-org-uuid',
organizationSecret: 'your-secret-key',
serviceId: 'your-service-uuid'
})
await pwa.initialize()
await pwa.registerForPush() // Anonymous registration
// Later, when user logs in:
const userToken = await getUserJWT()
pwa.setUserToken(userToken)
await pwa.registerForPush() // Re-register with user identityAPI Reference
Constructor
new CloudSignalPWA(config: PWAConfig)Config Options:
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| organizationId | string | Yes | CloudSignal organization UUID |
| organizationSecret | string | No* | Organization secret key (HMAC mode) |
| userToken | string | No* | JWT token from identity provider |
| onTokenExpired | function | No | Callback to refresh JWT on 401 |
| serviceId | string | Yes | PWA service UUID |
| serviceUrl | string | No | Service URL (default: https://pwa.cloudsignal.app) |
| debug | boolean | No | Enable debug logging |
| serviceWorker | object | No | Service worker config |
| heartbeat | object | No | Heartbeat config |
*Either organizationSecret or userToken must be provided
Initialization
// Initialize the PWA client
await pwa.initialize(): Promise<InitializeResult>
// Download service configuration
await pwa.downloadConfig(): Promise<PWAServiceConfig | null>Installation
// Show install prompt (Chrome/Edge)
await pwa.showInstallPrompt(): Promise<InstallResult>
// Check if PWA can be installed
pwa.canInstall(): boolean
// Check if PWA is already installed
pwa.isInstalled(): boolean
// Get installation state (full details)
pwa.getInstallationState(): InstallationState
// Returns: {
// isInstalled: boolean,
// canBeInstalled: boolean,
// needsManualInstall: boolean,
// showManualInstructions: boolean,
// installSteps: string[],
// displayMode: 'browser' | 'standalone' | 'minimal-ui' | 'fullscreen'
// }
// Get install steps for current platform
pwa.getInstallSteps(): string[]
// Register installation with backend (v1.2.3)
await pwa.registerInstallation(): Promise<{ registrationId: string } | null>
// Check if installation is registered (v1.2.3)
pwa.isInstallationRegistered(): boolean
// Get installation registration ID (v1.2.3)
pwa.getInstallationId(): string | nullPush Notifications
// Register for push notifications
await pwa.registerForPush(options?: RegisterOptions): Promise<Registration | null>
// Unregister from push notifications
await pwa.unregisterFromPush(): Promise<boolean>
// Update notification preferences
await pwa.updatePreferences(prefs: NotificationPreferences): Promise<boolean>
// Check registration status
await pwa.checkRegistrationStatus(): Promise<RegistrationStatusResponse | null>
// Request notification permission
await pwa.requestPermission(): Promise<NotificationPermission>
// Check if registered
pwa.isRegistered(): boolean
// Get registration ID
pwa.getRegistrationId(): string | nullDevice Information
// Get comprehensive device info (35+ fields)
pwa.getDeviceInfo(): DeviceInfo
// Get PWA capabilities
pwa.getCapabilities(): PWACapabilitiesHeartbeat
// Start heartbeat for online status tracking
pwa.startHeartbeat(): void
// Stop heartbeat
pwa.stopHeartbeat(): void
// Get current interval (may vary with network conditions)
pwa.getHeartbeatInterval(): number
// Get network connection info
pwa.getNetworkInfo(): NetworkConnectionInfo
// Get battery info (if available)
await pwa.getBatteryInfo(): Promise<BatteryInfo | null>Authentication (v1.2.0)
// Get current authentication mode
pwa.getAuthMode(): 'hmac' | 'jwt'
// Set/upgrade JWT token (for authenticated users)
pwa.setUserToken(token: string): void
// After calling setUserToken, call registerForPush() again to link registration to userScreen Wake Lock (v1.1.0)
// Request screen wake lock (prevents screen from sleeping)
await pwa.requestWakeLock(): Promise<boolean>
// Release wake lock
pwa.releaseWakeLock(): void
// Get current state
pwa.getWakeLockState(): WakeLockState
// Returns: { isSupported: boolean, isActive: boolean }Offline Request Queue (v1.1.0)
// Queue a request for later (when offline)
// Signature: queueRequest(url, method, options)
await pwa.queueOfflineRequest(
url: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
options?: {
headers?: Record<string, string>,
body?: string,
requestType?: 'registration' | 'heartbeat' | 'analytics' | 'preferences' | 'unregister' | 'custom',
priority?: number,
maxRetries?: number,
metadata?: Record<string, any>
}
): Promise<number | null> // Returns queue ID or null
// Process queued requests - returns array of results
await pwa.processOfflineQueue(): Promise<QueueProcessResult[]>
// QueueProcessResult: { id: number, success: boolean, statusCode?: number, error?: string, shouldRetry: boolean }
// Get queue statistics
await pwa.getOfflineQueueStats(): Promise<OfflineQueueStats>
// OfflineQueueStats: { totalQueued: number, byType: Record<string, number>, oldestRequest?: number, newestRequest?: number }
// Clear all queued requests
await pwa.clearOfflineQueue(): Promise<void>iOS Install Banner (v1.1.0)
// Show iOS install guidance banner
pwa.showIOSInstallBanner(): void
// Hide banner
pwa.hideIOSInstallBanner(): void
// Check if previously dismissed
pwa.wasIOSBannerDismissed(): boolean
// Reset dismissal state
pwa.resetIOSBannerDismissal(): voidBadge Management
// Clear app badge
pwa.clearBadge(): void
// Set app badge count
pwa.setBadge(count: number): voidEvents
// Subscribe to events
pwa.on(event: PWAEvent, handler: (data) => void): void
// Unsubscribe from events
pwa.off(event: PWAEvent, handler: (data) => void): voidAvailable Events:
Installation Events:
install:available- Install prompt is available. Handler:(data: { platforms: string[] }) => voidinstall:accepted- User accepted install. Handler:(data: { outcome: 'accepted', platform?: string }) => voidinstall:dismissed- User dismissed install. Handler:(data: { outcome: 'dismissed' }) => voidinstall:completed- PWA was installedinstall:registered- PWA installation registered with backend (v1.2.3). Handler:(data: { registrationId: string }) => voidinstall:error- Installation error occurred
Push Notification Events:
push:registered- Push registration successful. Handler:(data: { registrationId: string, endpoint: string }) => voidpush:unregistered- Push unregistration successfulpush:updated- Push registration updatedpush:error- Push operation failed. Handler:(data: { error: Error }) => voidpush:received- Push notification received. Handler:(data: { payload: NotificationPayload, timestamp: number }) => voidpush:clicked- Notification clicked. Handler:(data: { action?: string, data?: any, url?: string }) => void
Permission Events:
permission:granted- Notification permission grantedpermission:denied- Notification permission deniedpermission:prompt- Permission prompt shown
Service Worker Events:
sw:registered- Service worker registeredsw:updated- Service worker updatedsw:error- Service worker errorsw:activated- Service worker activated
Config Events:
config:loaded- Service config downloaded. Handler:(data: { config: PWAServiceConfig }) => voidconfig:error- Config download failed
Heartbeat Events:
heartbeat:started- Heartbeat startedheartbeat:stopped- Heartbeat stoppedheartbeat:sent- Heartbeat sent successfullyheartbeat:error- Heartbeat failedheartbeat:intervalChanged- Heartbeat interval adjusted (v1.1.0)heartbeat:pausedForBattery- Heartbeat paused due to low battery (v1.1.0)heartbeat:resumedFromBattery- Heartbeat resumed (v1.1.0)
Network Events:
network:online- Network came onlinenetwork:offline- Network went offline
Wake Lock Events (v1.1.0):
wakeLock:acquired- Screen wake lock acquiredwakeLock:released- Screen wake lock releasedwakeLock:error- Wake lock operation failed
Offline Queue Events (v1.1.0):
offlineQueue:queued- Request added to offline queueofflineQueue:processed- Queued request processedofflineQueue:flushed- All queued requests processed
iOS Banner Events (v1.1.0):
iosBanner:shown- iOS install banner showniosBanner:dismissed- iOS install banner dismissediosBanner:installClicked- User clicked install on iOS banner
Authentication Events (v1.2.0):
auth:tokenUpdated- JWT token updated/upgraded
State Events:
state:changed- SDK state changed
Service Worker Setup
IMPORTANT: The service worker MUST be placed at your app's root (e.g., /service-worker.js or /public/service-worker.js) to ensure correct scope. Service workers can only control pages within their scope.
Copy the service worker to your app's root directory:
# If using npm
cp node_modules/@cloudsignal/pwa-sdk/dist/service-worker.js public/Or download from CDN:
curl -o public/service-worker.js https://cdn.cloudsignal.io/service-worker.v1.0.0.jsCustom Service Worker Path
If you need a different path or filename:
const pwa = new CloudSignalPWA({
// ...other config
serviceWorker: {
path: '/sw.js', // Custom path
scope: '/', // Must match or be parent of your app routes
autoRegister: true,
updateBehavior: 'auto' // 'prompt' | 'auto' | 'manual'
}
})Workbox/Serwist Compatibility
IMPORTANT: CloudSignal's service worker is not compatible with Workbox, Serwist, or other PWA service worker libraries. You must use one or the other.
Why? CloudSignal's service worker handles:
- Push notification reception and display
- Dynamic manifest downloading and caching
- Notification click routing
- Badge management
- Offline notification history
These features require full control of the service worker lifecycle.
If you're currently using Workbox/Serwist:
- Remove the existing service worker library (
@serwist/next,next-pwa,workbox-webpack-plugin, etc.) - Use CloudSignal's service worker instead
- If you need precaching, consider using the browser's native Cache API in your application code
If you need features from Workbox (like precaching): Open an issue on GitHub - we may add these capabilities to the CloudSignal service worker in future versions.
PWA Manifest
Create a manifest.json in your app's root:
{
"name": "Your App Name",
"short_name": "App",
"start_url": "/",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff",
"icons": [
{
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}Add to your HTML:
<link rel="manifest" href="/manifest.json">Device Information
The SDK detects 35+ device attributes:
const info = pwa.getDeviceInfo()
// Operating System
info.os // 'iOS', 'Android', 'Windows', 'macOS', 'Linux'
info.osVersion // '17.2', '14.0', 'Windows 10/11'
// Device
info.deviceType // 'iPhone', 'iPad', 'Phone', 'Tablet', 'Desktop'
info.deviceModel // 'iPhone 15 Pro', 'Samsung Galaxy S24'
info.isMobile // true/false
info.isTablet // true/false
info.isDesktop // true/false
// Browser
info.browser // 'Chrome', 'Safari', 'Firefox', 'Edge'
info.browserVersion // '120.0.6099.43'
// PWA Capabilities
info.supportLevel // 'full', 'partial', 'basic', 'none'
info.hasPushManager // true/false
info.hasServiceWorker // true/false
info.hasBadgeAPI // true/false
info.notificationPermission // 'granted', 'denied', 'default'
// Network
info.isOnline // true/false
info.connectionType // '4g', '3g', '2g', 'unknown'Framework Integration Examples
Next.js (App Router)
IMPORTANT: The SDK uses browser APIs and must be loaded client-side only.
// components/CloudSignalProvider.tsx
'use client'
import { useEffect, useState, createContext, useContext, ReactNode } from 'react'
import type { CloudSignalPWA as CloudSignalPWAType } from '@cloudsignal/pwa-sdk'
type PWAContextType = {
pwa: CloudSignalPWAType | null
isInitialized: boolean
canInstall: boolean
isRegistered: boolean
}
const PWAContext = createContext<PWAContextType>({
pwa: null,
isInitialized: false,
canInstall: false,
isRegistered: false
})
export function CloudSignalProvider({ children }: { children: ReactNode }) {
const [pwa, setPwa] = useState<CloudSignalPWAType | null>(null)
const [isInitialized, setIsInitialized] = useState(false)
const [canInstall, setCanInstall] = useState(false)
const [isRegistered, setIsRegistered] = useState(false)
useEffect(() => {
// Dynamic import - required for Next.js App Router
const initPWA = async () => {
try {
const { CloudSignalPWA } = await import('@cloudsignal/pwa-sdk')
const instance = new CloudSignalPWA({
organizationId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_ID!,
organizationSecret: process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_SECRET!,
serviceId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_SERVICE_ID!,
debug: process.env.NODE_ENV === 'development'
})
await instance.initialize()
setPwa(instance)
setIsInitialized(true)
setCanInstall(instance.canInstall())
setIsRegistered(instance.isRegistered())
// Set up event listeners
instance.on('install:available', () => setCanInstall(true))
instance.on('install:completed', () => setCanInstall(false))
instance.on('push:registered', () => setIsRegistered(true))
instance.on('push:unregistered', () => setIsRegistered(false))
} catch (error) {
console.error('Failed to initialize CloudSignal PWA:', error)
}
}
initPWA()
}, [])
return (
<PWAContext.Provider value={{ pwa, isInitialized, canInstall, isRegistered }}>
{children}
</PWAContext.Provider>
)
}
export const usePWA = () => useContext(PWAContext)// app/layout.tsx
import { CloudSignalProvider } from '@/components/CloudSignalProvider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<CloudSignalProvider>
{children}
</CloudSignalProvider>
</body>
</html>
)
}// components/InstallButton.tsx
'use client'
import { usePWA } from './CloudSignalProvider'
export function InstallButton() {
const { pwa, canInstall } = usePWA()
if (!canInstall) return null
const handleInstall = async () => {
const result = await pwa?.showInstallPrompt()
if (result?.accepted) {
console.log('App installed!')
}
}
return <button onClick={handleInstall}>Install App</button>
}Next.js with Supabase Auth
// components/CloudSignalSupabaseProvider.tsx
'use client'
import { useEffect, useState, createContext, useContext, ReactNode } from 'react'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import type { CloudSignalPWA as CloudSignalPWAType } from '@cloudsignal/pwa-sdk'
const supabase = createClientComponentClient()
type PWAContextType = {
pwa: CloudSignalPWAType | null
isInitialized: boolean
}
const PWAContext = createContext<PWAContextType>({ pwa: null, isInitialized: false })
export function CloudSignalSupabaseProvider({ children }: { children: ReactNode }) {
const [pwa, setPwa] = useState<CloudSignalPWAType | null>(null)
const [isInitialized, setIsInitialized] = useState(false)
useEffect(() => {
let instance: CloudSignalPWAType | null = null
const initPWA = async () => {
const { CloudSignalPWA } = await import('@cloudsignal/pwa-sdk')
const { data: { session } } = await supabase.auth.getSession()
instance = new CloudSignalPWA({
organizationId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_ID!,
serviceId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_SERVICE_ID!,
// Use JWT if user is logged in, fall back to HMAC
userToken: session?.access_token,
organizationSecret: !session ? process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_SECRET : undefined,
onTokenExpired: async () => {
const { data } = await supabase.auth.refreshSession()
return data.session?.access_token || ''
}
})
await instance.initialize()
setPwa(instance)
setIsInitialized(true)
}
initPWA()
// Listen for auth changes to update token
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event, session) => {
if (instance && session?.access_token) {
instance.setUserToken(session.access_token)
// Re-register to link to user
await instance.registerForPush()
}
}
)
return () => {
subscription.unsubscribe()
}
}, [])
return (
<PWAContext.Provider value={{ pwa, isInitialized }}>
{children}
</PWAContext.Provider>
)
}
export const usePWA = () => useContext(PWAContext)React (Create React App / Vite)
import { useEffect, useState } from 'react'
import { CloudSignalPWA } from '@cloudsignal/pwa-sdk'
const pwa = new CloudSignalPWA({
organizationId: process.env.REACT_APP_ORG_ID,
organizationSecret: process.env.REACT_APP_ORG_SECRET,
serviceId: process.env.REACT_APP_SERVICE_ID
})
function App() {
const [canInstall, setCanInstall] = useState(false)
const [isRegistered, setIsRegistered] = useState(false)
useEffect(() => {
pwa.initialize().then(() => {
setCanInstall(pwa.canInstall())
setIsRegistered(pwa.isRegistered())
})
pwa.on('install:available', () => setCanInstall(true))
pwa.on('push:registered', () => setIsRegistered(true))
}, [])
const handleInstall = async () => {
const result = await pwa.showInstallPrompt()
if (result.accepted) {
setCanInstall(false)
}
}
const handleEnableNotifications = async () => {
const registration = await pwa.registerForPush({
userEmail: '[email protected]'
})
if (registration) {
setIsRegistered(true)
}
}
return (
<div>
{canInstall && (
<button onClick={handleInstall}>Install App</button>
)}
{!isRegistered && (
<button onClick={handleEnableNotifications}>
Enable Notifications
</button>
)}
</div>
)
}Browser Support
| Browser | PWA Install | Push Notifications | |---------|-------------|-------------------| | Chrome | ✅ | ✅ | | Edge | ✅ | ✅ | | Firefox | ❌ | ✅ | | Safari (macOS) | ❌ | ✅ (macOS 13+) | | Safari (iOS) | Manual | ✅ (iOS 16.4+) | | Samsung Internet | ✅ | ✅ |
TypeScript
Full TypeScript support with exported types:
import {
CloudSignalPWA,
PWAConfig,
DeviceInfo,
InstallationState,
Registration,
PWAEvent,
// v1.1.0 types
WakeLockState,
WakeLockConfig,
OfflineQueueConfig,
IOSInstallBannerConfig,
NetworkConnectionInfo,
BatteryInfo,
QueueProcessResult,
OfflineQueueStats,
} from '@cloudsignal/pwa-sdk'v1.1.0 Configuration
const pwa = new CloudSignalPWA({
organizationId: 'your-org-uuid',
organizationSecret: 'your-secret-key',
serviceId: 'your-service-uuid',
// Wake Lock (prevents screen sleep)
wakeLock: {
enabled: true,
autoRequest: false, // Request manually when needed
reacquireOnVisibility: true,
},
// Offline Queue (stores failed requests)
offlineQueue: {
enabled: true,
maxQueueSize: 100,
maxAgeTTLHours: 24,
autoProcessOnOnline: true,
},
// iOS Install Banner
iosInstallBanner: {
enabled: true,
appName: 'My PWA',
showOnFirstVisit: true,
showDelay: 3000,
dismissRememberDays: 7,
},
// Notification Analytics
notificationAnalytics: {
enabled: true,
endpoint: 'https://api.example.com/analytics', // Optional custom endpoint
},
})Troubleshooting
Service Worker Issues
Problem: Service worker not registering
DOMException: Failed to register a ServiceWorkerSolutions:
- Ensure service worker is at root path (
/service-worker.js) - Check HTTPS is enabled (required except on localhost)
- Verify service worker path in config matches actual file location
- Check browser console for detailed error
Problem: Push notifications not received after service worker update
Solution: The SDK handles this automatically with updateBehavior: 'auto'. If using manual mode, call pwa.getServiceWorkerManager().update() after deploy.
Permission Issues
Problem: Permission prompt never appears
Causes:
- User previously denied permission (check
pwa.getDeviceInfo().notificationPermission) - Site not served over HTTPS
- Browser settings blocking notifications globally
Solution for denied permission:
const deviceInfo = pwa.getDeviceInfo()
if (deviceInfo.notificationPermission === 'denied') {
// Guide user to browser settings
alert('Please enable notifications in your browser settings')
}iOS-Specific Issues
Problem: Install prompt not showing on iOS Safari
Explanation: iOS Safari doesn't support the Web App Install Banner API. Use the iOS Install Banner feature:
const pwa = new CloudSignalPWA({
// ...config
iosInstallBanner: {
enabled: true,
appName: 'My App',
showOnFirstVisit: true
}
})Problem: Push notifications not working on iOS
Requirements:
- iOS 16.4+
- PWA must be installed to home screen
- User must grant permission after installation
Token/Auth Issues
Problem: 401 errors after token expires
Solution: Implement onTokenExpired callback:
const pwa = new CloudSignalPWA({
// ...config
onTokenExpired: async () => {
// Your token refresh logic
const newToken = await refreshMyToken()
return newToken
}
})Problem: Registration not linked to user after login
Solution: Call setUserToken() followed by registerForPush():
pwa.setUserToken(newJWT)
await pwa.registerForPush() // Re-registers with user identityDebug Mode
Enable debug logging to troubleshoot issues:
const pwa = new CloudSignalPWA({
// ...config
debug: true // Logs all SDK operations to console
})Production Checklist
Before deploying to production, verify:
- [ ] HTTPS enabled - Required for service workers and push notifications
- [ ] Service worker at root -
/service-worker.jsor/public/service-worker.js - [ ] Manifest.json configured - Icons, name, start_url, display mode
- [ ] Environment variables set - Organization ID, Service ID, Secret/JWT config
- [ ] VAPID keys configured - In CloudSignal dashboard
- [ ] Test on target devices - iOS Safari, Chrome, Firefox, Edge
- [ ] Test install flow - Both automatic prompt and iOS manual instructions
- [ ] Test push notifications - Send test notification from dashboard
- [ ] Test offline behavior - Verify offline queue and reconnection
- [ ] Token refresh tested - If using JWT, verify refresh flow works
- [ ] Error handling - Graceful degradation when features unavailable
Migration from Other Services
From Firebase Cloud Messaging (FCM)
CloudSignal uses Web Push directly (not FCM). Key differences:
- No Firebase SDK dependency
- VAPID keys instead of FCM server key
- Direct Web Push Protocol
// Before (FCM)
import { getMessaging, getToken } from 'firebase/messaging'
const token = await getToken(messaging, { vapidKey: '...' })
// After (CloudSignal)
import { CloudSignalPWA } from '@cloudsignal/pwa-sdk'
const registration = await pwa.registerForPush()From OneSignal
Remove OneSignal SDK and replace with CloudSignal:
// Before
OneSignal.push(['init', { appId: '...' }])
// After
const pwa = new CloudSignalPWA({ organizationId: '...', serviceId: '...' })
await pwa.initialize()License
MIT License - Copyright (c) 2024-2025 CloudSignal
