@cloudsignal/pwa-sdk 1.2.3 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +502 -38
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
21
21
|
- `initialize()` now auto-calls `registerInstallation()` when PWA is detected as installed
|
|
22
22
|
- Prevents duplicate registrations using localStorage tracking
|
|
23
23
|
|
|
24
|
+
### Documentation
|
|
25
|
+
|
|
26
|
+
- **Type Corrections**: Fixed `registration.registrationId`, `InstallationState` object type, `queueOfflineRequest` signature, `processOfflineQueue` return type
|
|
27
|
+
- **Prerequisites Section**: Added environment variable configuration guide
|
|
28
|
+
- **Service Worker Setup**: Added root placement guidance, scope configuration, Workbox/Serwist coexistence patterns
|
|
29
|
+
- **Framework Integrations**: Added Next.js App Router guide with dynamic imports, Supabase Auth integration
|
|
30
|
+
- **Event Documentation**: Complete event list with handler signatures
|
|
31
|
+
- **Troubleshooting**: Service worker, permission, iOS, and token/auth issues
|
|
32
|
+
- **Production Checklist**: Pre-deployment verification steps
|
|
33
|
+
- **Migration Guides**: From Firebase Cloud Messaging and OneSignal
|
|
34
|
+
|
|
24
35
|
### Backend Support
|
|
25
36
|
|
|
26
37
|
- `/api/v1/registration/install-only` endpoint now supports JWT authentication
|
package/README.md
CHANGED
|
@@ -12,6 +12,12 @@ Progressive Web App SDK for CloudSignal platform with push notifications, instal
|
|
|
12
12
|
- **Service Worker** - Badge management, notification history, offline support
|
|
13
13
|
- **TypeScript** - Full type definitions included
|
|
14
14
|
|
|
15
|
+
### v1.2.3 Features
|
|
16
|
+
- **Auto Installation Tracking** - Automatically registers PWA installations with backend when detected
|
|
17
|
+
- `registerInstallation()` method for manual installation tracking
|
|
18
|
+
- `isInstallationRegistered()` / `getInstallationId()` helper methods
|
|
19
|
+
- `install:registered` event for installation tracking
|
|
20
|
+
|
|
15
21
|
### v1.2.0 Features
|
|
16
22
|
- **JWT Authentication** - User-linked registrations via JWT tokens (Supabase, Firebase, Auth0, Clerk)
|
|
17
23
|
- **Dual Auth Mode** - HMAC for anonymous users, JWT for authenticated users
|
|
@@ -44,6 +50,24 @@ npm install @cloudsignal/pwa-sdk
|
|
|
44
50
|
<script src="https://cdn.cloudsignal.io/cloudsignal-pwa.v1.0.0.js"></script>
|
|
45
51
|
```
|
|
46
52
|
|
|
53
|
+
## Prerequisites
|
|
54
|
+
|
|
55
|
+
### Environment Variables
|
|
56
|
+
|
|
57
|
+
Set up your environment variables before using the SDK:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# .env.local (Next.js) or .env
|
|
61
|
+
NEXT_PUBLIC_CLOUDSIGNAL_ORG_ID=your-org-uuid
|
|
62
|
+
NEXT_PUBLIC_CLOUDSIGNAL_SERVICE_ID=your-service-uuid
|
|
63
|
+
NEXT_PUBLIC_CLOUDSIGNAL_ORG_SECRET=your-secret-key # For HMAC mode only
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Where to get these values:**
|
|
67
|
+
1. **Organization ID** - From CloudSignal dashboard → Organization Settings
|
|
68
|
+
2. **Service ID** - From CloudSignal dashboard → PWA Services → Your Service
|
|
69
|
+
3. **Organization Secret** - From CloudSignal dashboard → API Keys (for HMAC mode)
|
|
70
|
+
|
|
47
71
|
## Quick Start
|
|
48
72
|
|
|
49
73
|
### Basic Usage
|
|
@@ -65,8 +89,9 @@ await pwa.initialize()
|
|
|
65
89
|
const deviceInfo = pwa.getDeviceInfo()
|
|
66
90
|
console.log('Device:', deviceInfo.deviceModel, deviceInfo.browser)
|
|
67
91
|
|
|
68
|
-
// Check installation state
|
|
69
|
-
|
|
92
|
+
// Check installation state - returns InstallationState object
|
|
93
|
+
const installState = pwa.getInstallationState()
|
|
94
|
+
if (installState.canBeInstalled && !installState.isInstalled) {
|
|
70
95
|
await pwa.showInstallPrompt()
|
|
71
96
|
}
|
|
72
97
|
|
|
@@ -74,6 +99,11 @@ if (pwa.canInstall()) {
|
|
|
74
99
|
const registration = await pwa.registerForPush({
|
|
75
100
|
userEmail: 'user@example.com'
|
|
76
101
|
})
|
|
102
|
+
|
|
103
|
+
// Access registration ID (note: registrationId, not id)
|
|
104
|
+
if (registration) {
|
|
105
|
+
console.log('Registered with ID:', registration.registrationId)
|
|
106
|
+
}
|
|
77
107
|
```
|
|
78
108
|
|
|
79
109
|
### CDN Usage
|
|
@@ -183,11 +213,28 @@ pwa.canInstall(): boolean
|
|
|
183
213
|
// Check if PWA is already installed
|
|
184
214
|
pwa.isInstalled(): boolean
|
|
185
215
|
|
|
186
|
-
// Get installation state
|
|
216
|
+
// Get installation state (full details)
|
|
187
217
|
pwa.getInstallationState(): InstallationState
|
|
218
|
+
// Returns: {
|
|
219
|
+
// isInstalled: boolean,
|
|
220
|
+
// canBeInstalled: boolean,
|
|
221
|
+
// needsManualInstall: boolean,
|
|
222
|
+
// showManualInstructions: boolean,
|
|
223
|
+
// installSteps: string[],
|
|
224
|
+
// displayMode: 'browser' | 'standalone' | 'minimal-ui' | 'fullscreen'
|
|
225
|
+
// }
|
|
188
226
|
|
|
189
227
|
// Get install steps for current platform
|
|
190
228
|
pwa.getInstallSteps(): string[]
|
|
229
|
+
|
|
230
|
+
// Register installation with backend (v1.2.3)
|
|
231
|
+
await pwa.registerInstallation(): Promise<{ registrationId: string } | null>
|
|
232
|
+
|
|
233
|
+
// Check if installation is registered (v1.2.3)
|
|
234
|
+
pwa.isInstallationRegistered(): boolean
|
|
235
|
+
|
|
236
|
+
// Get installation registration ID (v1.2.3)
|
|
237
|
+
pwa.getInstallationId(): string | null
|
|
191
238
|
```
|
|
192
239
|
|
|
193
240
|
### Push Notifications
|
|
@@ -273,13 +320,27 @@ pwa.getWakeLockState(): WakeLockState
|
|
|
273
320
|
|
|
274
321
|
```typescript
|
|
275
322
|
// Queue a request for later (when offline)
|
|
276
|
-
|
|
323
|
+
// Signature: queueRequest(url, method, options)
|
|
324
|
+
await pwa.queueOfflineRequest(
|
|
325
|
+
url: string,
|
|
326
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
|
|
327
|
+
options?: {
|
|
328
|
+
headers?: Record<string, string>,
|
|
329
|
+
body?: string,
|
|
330
|
+
requestType?: 'registration' | 'heartbeat' | 'analytics' | 'preferences' | 'unregister' | 'custom',
|
|
331
|
+
priority?: number,
|
|
332
|
+
maxRetries?: number,
|
|
333
|
+
metadata?: Record<string, any>
|
|
334
|
+
}
|
|
335
|
+
): Promise<number | null> // Returns queue ID or null
|
|
277
336
|
|
|
278
|
-
// Process queued requests
|
|
279
|
-
await pwa.processOfflineQueue(): Promise<QueueProcessResult>
|
|
337
|
+
// Process queued requests - returns array of results
|
|
338
|
+
await pwa.processOfflineQueue(): Promise<QueueProcessResult[]>
|
|
339
|
+
// QueueProcessResult: { id: number, success: boolean, statusCode?: number, error?: string, shouldRetry: boolean }
|
|
280
340
|
|
|
281
341
|
// Get queue statistics
|
|
282
342
|
await pwa.getOfflineQueueStats(): Promise<OfflineQueueStats>
|
|
343
|
+
// OfflineQueueStats: { totalQueued: number, byType: Record<string, number>, oldestRequest?: number, newestRequest?: number }
|
|
283
344
|
|
|
284
345
|
// Clear all queued requests
|
|
285
346
|
await pwa.clearOfflineQueue(): Promise<void>
|
|
@@ -323,40 +384,75 @@ pwa.off(event: PWAEvent, handler: (data) => void): void
|
|
|
323
384
|
|
|
324
385
|
**Available Events:**
|
|
325
386
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
387
|
+
**Installation Events:**
|
|
388
|
+
- `install:available` - Install prompt is available. Handler: `(data: { platforms: string[] }) => void`
|
|
389
|
+
- `install:accepted` - User accepted install. Handler: `(data: { outcome: 'accepted', platform?: string }) => void`
|
|
390
|
+
- `install:dismissed` - User dismissed install. Handler: `(data: { outcome: 'dismissed' }) => void`
|
|
391
|
+
- `install:completed` - PWA was installed
|
|
392
|
+
- `install:registered` - PWA installation registered with backend (v1.2.3). Handler: `(data: { registrationId: string }) => void`
|
|
393
|
+
- `install:error` - Installation error occurred
|
|
394
|
+
|
|
395
|
+
**Push Notification Events:**
|
|
396
|
+
- `push:registered` - Push registration successful. Handler: `(data: { registrationId: string, endpoint: string }) => void`
|
|
397
|
+
- `push:unregistered` - Push unregistration successful
|
|
398
|
+
- `push:updated` - Push registration updated
|
|
399
|
+
- `push:error` - Push operation failed. Handler: `(data: { error: Error }) => void`
|
|
400
|
+
- `push:received` - Push notification received. Handler: `(data: { payload: NotificationPayload, timestamp: number }) => void`
|
|
401
|
+
- `push:clicked` - Notification clicked. Handler: `(data: { action?: string, data?: any, url?: string }) => void`
|
|
402
|
+
|
|
403
|
+
**Permission Events:**
|
|
404
|
+
- `permission:granted` - Notification permission granted
|
|
405
|
+
- `permission:denied` - Notification permission denied
|
|
406
|
+
- `permission:prompt` - Permission prompt shown
|
|
407
|
+
|
|
408
|
+
**Service Worker Events:**
|
|
409
|
+
- `sw:registered` - Service worker registered
|
|
410
|
+
- `sw:updated` - Service worker updated
|
|
411
|
+
- `sw:error` - Service worker error
|
|
412
|
+
- `sw:activated` - Service worker activated
|
|
413
|
+
|
|
414
|
+
**Config Events:**
|
|
415
|
+
- `config:loaded` - Service config downloaded. Handler: `(data: { config: PWAServiceConfig }) => void`
|
|
416
|
+
- `config:error` - Config download failed
|
|
417
|
+
|
|
418
|
+
**Heartbeat Events:**
|
|
419
|
+
- `heartbeat:started` - Heartbeat started
|
|
420
|
+
- `heartbeat:stopped` - Heartbeat stopped
|
|
421
|
+
- `heartbeat:sent` - Heartbeat sent successfully
|
|
422
|
+
- `heartbeat:error` - Heartbeat failed
|
|
423
|
+
- `heartbeat:intervalChanged` - Heartbeat interval adjusted (v1.1.0)
|
|
424
|
+
- `heartbeat:pausedForBattery` - Heartbeat paused due to low battery (v1.1.0)
|
|
425
|
+
- `heartbeat:resumedFromBattery` - Heartbeat resumed (v1.1.0)
|
|
426
|
+
|
|
427
|
+
**Network Events:**
|
|
428
|
+
- `network:online` - Network came online
|
|
429
|
+
- `network:offline` - Network went offline
|
|
430
|
+
|
|
431
|
+
**Wake Lock Events (v1.1.0):**
|
|
432
|
+
- `wakeLock:acquired` - Screen wake lock acquired
|
|
433
|
+
- `wakeLock:released` - Screen wake lock released
|
|
434
|
+
- `wakeLock:error` - Wake lock operation failed
|
|
435
|
+
|
|
436
|
+
**Offline Queue Events (v1.1.0):**
|
|
437
|
+
- `offlineQueue:queued` - Request added to offline queue
|
|
438
|
+
- `offlineQueue:processed` - Queued request processed
|
|
439
|
+
- `offlineQueue:flushed` - All queued requests processed
|
|
440
|
+
|
|
441
|
+
**iOS Banner Events (v1.1.0):**
|
|
442
|
+
- `iosBanner:shown` - iOS install banner shown
|
|
443
|
+
- `iosBanner:dismissed` - iOS install banner dismissed
|
|
444
|
+
- `iosBanner:installClicked` - User clicked install on iOS banner
|
|
445
|
+
|
|
446
|
+
**Authentication Events (v1.2.0):**
|
|
447
|
+
- `auth:tokenUpdated` - JWT token updated/upgraded
|
|
448
|
+
|
|
449
|
+
**State Events:**
|
|
450
|
+
- `state:changed` - SDK state changed
|
|
357
451
|
|
|
358
452
|
## Service Worker Setup
|
|
359
453
|
|
|
454
|
+
**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.
|
|
455
|
+
|
|
360
456
|
Copy the service worker to your app's root directory:
|
|
361
457
|
|
|
362
458
|
```bash
|
|
@@ -370,6 +466,43 @@ Or download from CDN:
|
|
|
370
466
|
curl -o public/service-worker.js https://cdn.cloudsignal.io/service-worker.v1.0.0.js
|
|
371
467
|
```
|
|
372
468
|
|
|
469
|
+
### Custom Service Worker Path
|
|
470
|
+
|
|
471
|
+
If you need a different path or filename:
|
|
472
|
+
|
|
473
|
+
```javascript
|
|
474
|
+
const pwa = new CloudSignalPWA({
|
|
475
|
+
// ...other config
|
|
476
|
+
serviceWorker: {
|
|
477
|
+
path: '/sw.js', // Custom path
|
|
478
|
+
scope: '/', // Must match or be parent of your app routes
|
|
479
|
+
autoRegister: true,
|
|
480
|
+
updateBehavior: 'auto' // 'prompt' | 'auto' | 'manual'
|
|
481
|
+
}
|
|
482
|
+
})
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Workbox/Serwist Compatibility
|
|
486
|
+
|
|
487
|
+
**IMPORTANT:** CloudSignal's service worker is **not compatible** with Workbox, Serwist, or other PWA service worker libraries. You must use one or the other.
|
|
488
|
+
|
|
489
|
+
**Why?** CloudSignal's service worker handles:
|
|
490
|
+
- Push notification reception and display
|
|
491
|
+
- Dynamic manifest downloading and caching
|
|
492
|
+
- Notification click routing
|
|
493
|
+
- Badge management
|
|
494
|
+
- Offline notification history
|
|
495
|
+
|
|
496
|
+
These features require full control of the service worker lifecycle.
|
|
497
|
+
|
|
498
|
+
**If you're currently using Workbox/Serwist:**
|
|
499
|
+
1. Remove the existing service worker library (`@serwist/next`, `next-pwa`, `workbox-webpack-plugin`, etc.)
|
|
500
|
+
2. Use CloudSignal's service worker instead
|
|
501
|
+
3. If you need precaching, consider using the browser's native Cache API in your application code
|
|
502
|
+
|
|
503
|
+
**If you need features from Workbox (like precaching):**
|
|
504
|
+
Open an issue on GitHub - we may add these capabilities to the CloudSignal service worker in future versions.
|
|
505
|
+
|
|
373
506
|
## PWA Manifest
|
|
374
507
|
|
|
375
508
|
Create a `manifest.json` in your app's root:
|
|
@@ -437,7 +570,197 @@ info.isOnline // true/false
|
|
|
437
570
|
info.connectionType // '4g', '3g', '2g', 'unknown'
|
|
438
571
|
```
|
|
439
572
|
|
|
440
|
-
##
|
|
573
|
+
## Framework Integration Examples
|
|
574
|
+
|
|
575
|
+
### Next.js (App Router)
|
|
576
|
+
|
|
577
|
+
**IMPORTANT:** The SDK uses browser APIs and must be loaded client-side only.
|
|
578
|
+
|
|
579
|
+
```tsx
|
|
580
|
+
// components/CloudSignalProvider.tsx
|
|
581
|
+
'use client'
|
|
582
|
+
|
|
583
|
+
import { useEffect, useState, createContext, useContext, ReactNode } from 'react'
|
|
584
|
+
import type { CloudSignalPWA as CloudSignalPWAType } from '@cloudsignal/pwa-sdk'
|
|
585
|
+
|
|
586
|
+
type PWAContextType = {
|
|
587
|
+
pwa: CloudSignalPWAType | null
|
|
588
|
+
isInitialized: boolean
|
|
589
|
+
canInstall: boolean
|
|
590
|
+
isRegistered: boolean
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const PWAContext = createContext<PWAContextType>({
|
|
594
|
+
pwa: null,
|
|
595
|
+
isInitialized: false,
|
|
596
|
+
canInstall: false,
|
|
597
|
+
isRegistered: false
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
export function CloudSignalProvider({ children }: { children: ReactNode }) {
|
|
601
|
+
const [pwa, setPwa] = useState<CloudSignalPWAType | null>(null)
|
|
602
|
+
const [isInitialized, setIsInitialized] = useState(false)
|
|
603
|
+
const [canInstall, setCanInstall] = useState(false)
|
|
604
|
+
const [isRegistered, setIsRegistered] = useState(false)
|
|
605
|
+
|
|
606
|
+
useEffect(() => {
|
|
607
|
+
// Dynamic import - required for Next.js App Router
|
|
608
|
+
const initPWA = async () => {
|
|
609
|
+
try {
|
|
610
|
+
const { CloudSignalPWA } = await import('@cloudsignal/pwa-sdk')
|
|
611
|
+
|
|
612
|
+
const instance = new CloudSignalPWA({
|
|
613
|
+
organizationId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_ID!,
|
|
614
|
+
organizationSecret: process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_SECRET!,
|
|
615
|
+
serviceId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_SERVICE_ID!,
|
|
616
|
+
debug: process.env.NODE_ENV === 'development'
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
await instance.initialize()
|
|
620
|
+
|
|
621
|
+
setPwa(instance)
|
|
622
|
+
setIsInitialized(true)
|
|
623
|
+
setCanInstall(instance.canInstall())
|
|
624
|
+
setIsRegistered(instance.isRegistered())
|
|
625
|
+
|
|
626
|
+
// Set up event listeners
|
|
627
|
+
instance.on('install:available', () => setCanInstall(true))
|
|
628
|
+
instance.on('install:completed', () => setCanInstall(false))
|
|
629
|
+
instance.on('push:registered', () => setIsRegistered(true))
|
|
630
|
+
instance.on('push:unregistered', () => setIsRegistered(false))
|
|
631
|
+
} catch (error) {
|
|
632
|
+
console.error('Failed to initialize CloudSignal PWA:', error)
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
initPWA()
|
|
637
|
+
}, [])
|
|
638
|
+
|
|
639
|
+
return (
|
|
640
|
+
<PWAContext.Provider value={{ pwa, isInitialized, canInstall, isRegistered }}>
|
|
641
|
+
{children}
|
|
642
|
+
</PWAContext.Provider>
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export const usePWA = () => useContext(PWAContext)
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
```tsx
|
|
650
|
+
// app/layout.tsx
|
|
651
|
+
import { CloudSignalProvider } from '@/components/CloudSignalProvider'
|
|
652
|
+
|
|
653
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
654
|
+
return (
|
|
655
|
+
<html>
|
|
656
|
+
<body>
|
|
657
|
+
<CloudSignalProvider>
|
|
658
|
+
{children}
|
|
659
|
+
</CloudSignalProvider>
|
|
660
|
+
</body>
|
|
661
|
+
</html>
|
|
662
|
+
)
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
```tsx
|
|
667
|
+
// components/InstallButton.tsx
|
|
668
|
+
'use client'
|
|
669
|
+
|
|
670
|
+
import { usePWA } from './CloudSignalProvider'
|
|
671
|
+
|
|
672
|
+
export function InstallButton() {
|
|
673
|
+
const { pwa, canInstall } = usePWA()
|
|
674
|
+
|
|
675
|
+
if (!canInstall) return null
|
|
676
|
+
|
|
677
|
+
const handleInstall = async () => {
|
|
678
|
+
const result = await pwa?.showInstallPrompt()
|
|
679
|
+
if (result?.accepted) {
|
|
680
|
+
console.log('App installed!')
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return <button onClick={handleInstall}>Install App</button>
|
|
685
|
+
}
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### Next.js with Supabase Auth
|
|
689
|
+
|
|
690
|
+
```tsx
|
|
691
|
+
// components/CloudSignalSupabaseProvider.tsx
|
|
692
|
+
'use client'
|
|
693
|
+
|
|
694
|
+
import { useEffect, useState, createContext, useContext, ReactNode } from 'react'
|
|
695
|
+
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
|
|
696
|
+
import type { CloudSignalPWA as CloudSignalPWAType } from '@cloudsignal/pwa-sdk'
|
|
697
|
+
|
|
698
|
+
const supabase = createClientComponentClient()
|
|
699
|
+
|
|
700
|
+
type PWAContextType = {
|
|
701
|
+
pwa: CloudSignalPWAType | null
|
|
702
|
+
isInitialized: boolean
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const PWAContext = createContext<PWAContextType>({ pwa: null, isInitialized: false })
|
|
706
|
+
|
|
707
|
+
export function CloudSignalSupabaseProvider({ children }: { children: ReactNode }) {
|
|
708
|
+
const [pwa, setPwa] = useState<CloudSignalPWAType | null>(null)
|
|
709
|
+
const [isInitialized, setIsInitialized] = useState(false)
|
|
710
|
+
|
|
711
|
+
useEffect(() => {
|
|
712
|
+
let instance: CloudSignalPWAType | null = null
|
|
713
|
+
|
|
714
|
+
const initPWA = async () => {
|
|
715
|
+
const { CloudSignalPWA } = await import('@cloudsignal/pwa-sdk')
|
|
716
|
+
const { data: { session } } = await supabase.auth.getSession()
|
|
717
|
+
|
|
718
|
+
instance = new CloudSignalPWA({
|
|
719
|
+
organizationId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_ID!,
|
|
720
|
+
serviceId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_SERVICE_ID!,
|
|
721
|
+
// Use JWT if user is logged in, fall back to HMAC
|
|
722
|
+
userToken: session?.access_token,
|
|
723
|
+
organizationSecret: !session ? process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_SECRET : undefined,
|
|
724
|
+
onTokenExpired: async () => {
|
|
725
|
+
const { data } = await supabase.auth.refreshSession()
|
|
726
|
+
return data.session?.access_token || ''
|
|
727
|
+
}
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
await instance.initialize()
|
|
731
|
+
setPwa(instance)
|
|
732
|
+
setIsInitialized(true)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
initPWA()
|
|
736
|
+
|
|
737
|
+
// Listen for auth changes to update token
|
|
738
|
+
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
|
739
|
+
async (event, session) => {
|
|
740
|
+
if (instance && session?.access_token) {
|
|
741
|
+
instance.setUserToken(session.access_token)
|
|
742
|
+
// Re-register to link to user
|
|
743
|
+
await instance.registerForPush()
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
return () => {
|
|
749
|
+
subscription.unsubscribe()
|
|
750
|
+
}
|
|
751
|
+
}, [])
|
|
752
|
+
|
|
753
|
+
return (
|
|
754
|
+
<PWAContext.Provider value={{ pwa, isInitialized }}>
|
|
755
|
+
{children}
|
|
756
|
+
</PWAContext.Provider>
|
|
757
|
+
)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export const usePWA = () => useContext(PWAContext)
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
### React (Create React App / Vite)
|
|
441
764
|
|
|
442
765
|
```jsx
|
|
443
766
|
import { useEffect, useState } from 'react'
|
|
@@ -569,6 +892,147 @@ const pwa = new CloudSignalPWA({
|
|
|
569
892
|
})
|
|
570
893
|
```
|
|
571
894
|
|
|
895
|
+
## Troubleshooting
|
|
896
|
+
|
|
897
|
+
### Service Worker Issues
|
|
898
|
+
|
|
899
|
+
**Problem:** Service worker not registering
|
|
900
|
+
```
|
|
901
|
+
DOMException: Failed to register a ServiceWorker
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
**Solutions:**
|
|
905
|
+
1. Ensure service worker is at root path (`/service-worker.js`)
|
|
906
|
+
2. Check HTTPS is enabled (required except on localhost)
|
|
907
|
+
3. Verify service worker path in config matches actual file location
|
|
908
|
+
4. Check browser console for detailed error
|
|
909
|
+
|
|
910
|
+
**Problem:** Push notifications not received after service worker update
|
|
911
|
+
|
|
912
|
+
**Solution:** The SDK handles this automatically with `updateBehavior: 'auto'`. If using manual mode, call `pwa.getServiceWorkerManager().update()` after deploy.
|
|
913
|
+
|
|
914
|
+
### Permission Issues
|
|
915
|
+
|
|
916
|
+
**Problem:** Permission prompt never appears
|
|
917
|
+
|
|
918
|
+
**Causes:**
|
|
919
|
+
1. User previously denied permission (check `pwa.getDeviceInfo().notificationPermission`)
|
|
920
|
+
2. Site not served over HTTPS
|
|
921
|
+
3. Browser settings blocking notifications globally
|
|
922
|
+
|
|
923
|
+
**Solution for denied permission:**
|
|
924
|
+
```javascript
|
|
925
|
+
const deviceInfo = pwa.getDeviceInfo()
|
|
926
|
+
if (deviceInfo.notificationPermission === 'denied') {
|
|
927
|
+
// Guide user to browser settings
|
|
928
|
+
alert('Please enable notifications in your browser settings')
|
|
929
|
+
}
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
### iOS-Specific Issues
|
|
933
|
+
|
|
934
|
+
**Problem:** Install prompt not showing on iOS Safari
|
|
935
|
+
|
|
936
|
+
**Explanation:** iOS Safari doesn't support the Web App Install Banner API. Use the iOS Install Banner feature:
|
|
937
|
+
```javascript
|
|
938
|
+
const pwa = new CloudSignalPWA({
|
|
939
|
+
// ...config
|
|
940
|
+
iosInstallBanner: {
|
|
941
|
+
enabled: true,
|
|
942
|
+
appName: 'My App',
|
|
943
|
+
showOnFirstVisit: true
|
|
944
|
+
}
|
|
945
|
+
})
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
**Problem:** Push notifications not working on iOS
|
|
949
|
+
|
|
950
|
+
**Requirements:**
|
|
951
|
+
- iOS 16.4+
|
|
952
|
+
- PWA must be installed to home screen
|
|
953
|
+
- User must grant permission after installation
|
|
954
|
+
|
|
955
|
+
### Token/Auth Issues
|
|
956
|
+
|
|
957
|
+
**Problem:** 401 errors after token expires
|
|
958
|
+
|
|
959
|
+
**Solution:** Implement `onTokenExpired` callback:
|
|
960
|
+
```javascript
|
|
961
|
+
const pwa = new CloudSignalPWA({
|
|
962
|
+
// ...config
|
|
963
|
+
onTokenExpired: async () => {
|
|
964
|
+
// Your token refresh logic
|
|
965
|
+
const newToken = await refreshMyToken()
|
|
966
|
+
return newToken
|
|
967
|
+
}
|
|
968
|
+
})
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
**Problem:** Registration not linked to user after login
|
|
972
|
+
|
|
973
|
+
**Solution:** Call `setUserToken()` followed by `registerForPush()`:
|
|
974
|
+
```javascript
|
|
975
|
+
pwa.setUserToken(newJWT)
|
|
976
|
+
await pwa.registerForPush() // Re-registers with user identity
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
### Debug Mode
|
|
980
|
+
|
|
981
|
+
Enable debug logging to troubleshoot issues:
|
|
982
|
+
```javascript
|
|
983
|
+
const pwa = new CloudSignalPWA({
|
|
984
|
+
// ...config
|
|
985
|
+
debug: true // Logs all SDK operations to console
|
|
986
|
+
})
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
## Production Checklist
|
|
990
|
+
|
|
991
|
+
Before deploying to production, verify:
|
|
992
|
+
|
|
993
|
+
- [ ] **HTTPS enabled** - Required for service workers and push notifications
|
|
994
|
+
- [ ] **Service worker at root** - `/service-worker.js` or `/public/service-worker.js`
|
|
995
|
+
- [ ] **Manifest.json configured** - Icons, name, start_url, display mode
|
|
996
|
+
- [ ] **Environment variables set** - Organization ID, Service ID, Secret/JWT config
|
|
997
|
+
- [ ] **VAPID keys configured** - In CloudSignal dashboard
|
|
998
|
+
- [ ] **Test on target devices** - iOS Safari, Chrome, Firefox, Edge
|
|
999
|
+
- [ ] **Test install flow** - Both automatic prompt and iOS manual instructions
|
|
1000
|
+
- [ ] **Test push notifications** - Send test notification from dashboard
|
|
1001
|
+
- [ ] **Test offline behavior** - Verify offline queue and reconnection
|
|
1002
|
+
- [ ] **Token refresh tested** - If using JWT, verify refresh flow works
|
|
1003
|
+
- [ ] **Error handling** - Graceful degradation when features unavailable
|
|
1004
|
+
|
|
1005
|
+
## Migration from Other Services
|
|
1006
|
+
|
|
1007
|
+
### From Firebase Cloud Messaging (FCM)
|
|
1008
|
+
|
|
1009
|
+
CloudSignal uses Web Push directly (not FCM). Key differences:
|
|
1010
|
+
- No Firebase SDK dependency
|
|
1011
|
+
- VAPID keys instead of FCM server key
|
|
1012
|
+
- Direct Web Push Protocol
|
|
1013
|
+
|
|
1014
|
+
```javascript
|
|
1015
|
+
// Before (FCM)
|
|
1016
|
+
import { getMessaging, getToken } from 'firebase/messaging'
|
|
1017
|
+
const token = await getToken(messaging, { vapidKey: '...' })
|
|
1018
|
+
|
|
1019
|
+
// After (CloudSignal)
|
|
1020
|
+
import { CloudSignalPWA } from '@cloudsignal/pwa-sdk'
|
|
1021
|
+
const registration = await pwa.registerForPush()
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
### From OneSignal
|
|
1025
|
+
|
|
1026
|
+
Remove OneSignal SDK and replace with CloudSignal:
|
|
1027
|
+
```javascript
|
|
1028
|
+
// Before
|
|
1029
|
+
OneSignal.push(['init', { appId: '...' }])
|
|
1030
|
+
|
|
1031
|
+
// After
|
|
1032
|
+
const pwa = new CloudSignalPWA({ organizationId: '...', serviceId: '...' })
|
|
1033
|
+
await pwa.initialize()
|
|
1034
|
+
```
|
|
1035
|
+
|
|
572
1036
|
## License
|
|
573
1037
|
|
|
574
1038
|
MIT License - Copyright (c) 2024-2025 CloudSignal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudsignal/pwa-sdk",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "CloudSignal PWA SDK - Progressive Web App features with push notifications, JWT/HMAC authentication, installation management, device tracking, offline queue, wake lock, and notification analytics",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.js",
|