@djangocfg/nextjs 2.1.36 → 2.1.38
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/README.md +146 -1
- package/dist/config/index.d.mts +7 -428
- package/dist/config/index.mjs +80 -396
- package/dist/config/index.mjs.map +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +80 -396
- package/dist/index.mjs.map +1 -1
- package/dist/plugin-DuRJ_Jq6.d.mts +100 -0
- package/dist/pwa/cli.d.mts +1 -0
- package/dist/pwa/cli.mjs +140 -0
- package/dist/pwa/cli.mjs.map +1 -0
- package/dist/pwa/index.d.mts +274 -0
- package/dist/pwa/index.mjs +327 -0
- package/dist/pwa/index.mjs.map +1 -0
- package/dist/pwa/server/index.d.mts +86 -0
- package/dist/pwa/server/index.mjs +175 -0
- package/dist/pwa/server/index.mjs.map +1 -0
- package/dist/pwa/server/routes.d.mts +2 -0
- package/dist/pwa/server/routes.mjs +149 -0
- package/dist/pwa/server/routes.mjs.map +1 -0
- package/dist/pwa/worker/index.d.mts +56 -0
- package/dist/pwa/worker/index.mjs +97 -0
- package/dist/pwa/worker/index.mjs.map +1 -0
- package/dist/routes-DXA29sS_.d.mts +68 -0
- package/package.json +39 -8
- package/src/config/createNextConfig.ts +9 -13
- package/src/config/index.ts +2 -20
- package/src/config/plugins/devStartup.ts +35 -36
- package/src/config/plugins/index.ts +1 -1
- package/src/config/utils/index.ts +0 -1
- package/src/index.ts +4 -0
- package/src/pwa/cli.ts +171 -0
- package/src/pwa/index.ts +9 -0
- package/src/pwa/manifest.ts +355 -0
- package/src/pwa/notifications.ts +192 -0
- package/src/pwa/plugin.ts +194 -0
- package/src/pwa/server/index.ts +23 -0
- package/src/pwa/server/push.ts +166 -0
- package/src/pwa/server/routes.ts +137 -0
- package/src/pwa/worker/index.ts +174 -0
- package/src/pwa/worker/package.json +3 -0
- package/src/config/plugins/pwa.ts +0 -616
- package/src/config/utils/manifest.ts +0 -214
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PWA Push Notifications Client Utilities
|
|
3
|
+
*
|
|
4
|
+
* Functions for requesting permission, subscribing to push notifications,
|
|
5
|
+
* and sending test notifications
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if push notifications are supported
|
|
10
|
+
*/
|
|
11
|
+
export function isPushNotificationSupported(): boolean {
|
|
12
|
+
return (
|
|
13
|
+
'serviceWorker' in navigator &&
|
|
14
|
+
'PushManager' in window &&
|
|
15
|
+
'Notification' in window
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get current notification permission status
|
|
21
|
+
*/
|
|
22
|
+
export function getNotificationPermission(): NotificationPermission {
|
|
23
|
+
if (!('Notification' in window)) {
|
|
24
|
+
return 'denied';
|
|
25
|
+
}
|
|
26
|
+
return Notification.permission;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Request notification permission from user
|
|
31
|
+
*/
|
|
32
|
+
export async function requestNotificationPermission(): Promise<NotificationPermission> {
|
|
33
|
+
if (!('Notification' in window)) {
|
|
34
|
+
throw new Error('Notifications are not supported');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const permission = await Notification.requestPermission();
|
|
38
|
+
return permission;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert base64 VAPID key to Uint8Array
|
|
43
|
+
*/
|
|
44
|
+
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|
45
|
+
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
|
46
|
+
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
47
|
+
|
|
48
|
+
const rawData = window.atob(base64);
|
|
49
|
+
const outputArray = new Uint8Array(rawData.length);
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < rawData.length; ++i) {
|
|
52
|
+
outputArray[i] = rawData.charCodeAt(i);
|
|
53
|
+
}
|
|
54
|
+
return outputArray;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface PushSubscriptionOptions {
|
|
58
|
+
/**
|
|
59
|
+
* VAPID public key (base64 encoded)
|
|
60
|
+
* Generate with: npx web-push generate-vapid-keys
|
|
61
|
+
*/
|
|
62
|
+
vapidPublicKey: string;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* User visible only (required for Chrome)
|
|
66
|
+
* @default true
|
|
67
|
+
*/
|
|
68
|
+
userVisibleOnly?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Subscribe to push notifications
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const subscription = await subscribeToPushNotifications({
|
|
77
|
+
* vapidPublicKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* // Send subscription to your backend
|
|
81
|
+
* await fetch('/api/push/subscribe', {
|
|
82
|
+
* method: 'POST',
|
|
83
|
+
* headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
* body: JSON.stringify(subscription),
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export async function subscribeToPushNotifications(
|
|
89
|
+
options: PushSubscriptionOptions
|
|
90
|
+
): Promise<PushSubscription> {
|
|
91
|
+
if (!isPushNotificationSupported()) {
|
|
92
|
+
throw new Error('Push notifications are not supported');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Request permission first
|
|
96
|
+
const permission = await requestNotificationPermission();
|
|
97
|
+
if (permission !== 'granted') {
|
|
98
|
+
throw new Error(`Notification permission ${permission}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get service worker registration
|
|
102
|
+
const registration = await navigator.serviceWorker.ready;
|
|
103
|
+
|
|
104
|
+
// Check if already subscribed
|
|
105
|
+
let subscription = await registration.pushManager.getSubscription();
|
|
106
|
+
|
|
107
|
+
if (subscription) {
|
|
108
|
+
return subscription;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Subscribe to push notifications
|
|
112
|
+
const convertedVapidKey = urlBase64ToUint8Array(options.vapidPublicKey);
|
|
113
|
+
|
|
114
|
+
subscription = await registration.pushManager.subscribe({
|
|
115
|
+
userVisibleOnly: options.userVisibleOnly ?? true,
|
|
116
|
+
applicationServerKey: convertedVapidKey as BufferSource,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return subscription;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Unsubscribe from push notifications
|
|
124
|
+
*/
|
|
125
|
+
export async function unsubscribeFromPushNotifications(): Promise<boolean> {
|
|
126
|
+
if (!isPushNotificationSupported()) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const registration = await navigator.serviceWorker.ready;
|
|
131
|
+
const subscription = await registration.pushManager.getSubscription();
|
|
132
|
+
|
|
133
|
+
if (subscription) {
|
|
134
|
+
return await subscription.unsubscribe();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get current push subscription
|
|
142
|
+
*/
|
|
143
|
+
export async function getPushSubscription(): Promise<PushSubscription | null> {
|
|
144
|
+
if (!isPushNotificationSupported()) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const registration = await navigator.serviceWorker.ready;
|
|
149
|
+
return await registration.pushManager.getSubscription();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Show a local notification (doesn't require push)
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* await showLocalNotification({
|
|
158
|
+
* title: 'Hello!',
|
|
159
|
+
* body: 'This is a test notification',
|
|
160
|
+
* icon: '/icon.png',
|
|
161
|
+
* data: { url: '/some-page' },
|
|
162
|
+
* });
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
export async function showLocalNotification(options: {
|
|
166
|
+
title: string;
|
|
167
|
+
body?: string;
|
|
168
|
+
icon?: string;
|
|
169
|
+
badge?: string;
|
|
170
|
+
tag?: string;
|
|
171
|
+
data?: any;
|
|
172
|
+
requireInteraction?: boolean;
|
|
173
|
+
}): Promise<void> {
|
|
174
|
+
if (!('Notification' in window)) {
|
|
175
|
+
throw new Error('Notifications are not supported');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const permission = await requestNotificationPermission();
|
|
179
|
+
if (permission !== 'granted') {
|
|
180
|
+
throw new Error(`Notification permission ${permission}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const registration = await navigator.serviceWorker.ready;
|
|
184
|
+
await registration.showNotification(options.title, {
|
|
185
|
+
body: options.body,
|
|
186
|
+
icon: options.icon,
|
|
187
|
+
badge: options.badge,
|
|
188
|
+
tag: options.tag,
|
|
189
|
+
data: options.data,
|
|
190
|
+
requireInteraction: options.requireInteraction,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PWA (Progressive Web App) Plugin
|
|
3
|
+
*
|
|
4
|
+
* Configures Serwist for service worker and offline support
|
|
5
|
+
* Modern PWA solution for Next.js 15+ with App Router
|
|
6
|
+
*
|
|
7
|
+
* @see https://serwist.pages.dev/
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { NextConfig } from 'next';
|
|
11
|
+
import { consola } from 'consola';
|
|
12
|
+
|
|
13
|
+
export interface PWAPluginOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Destination directory for service worker files
|
|
16
|
+
* @default 'public'
|
|
17
|
+
* @deprecated Use swDest instead
|
|
18
|
+
*/
|
|
19
|
+
dest?: string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Path to service worker source file (relative to project root)
|
|
23
|
+
* @default 'app/sw.ts'
|
|
24
|
+
*/
|
|
25
|
+
swSrc?: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Destination for compiled service worker
|
|
29
|
+
* @default 'public/sw.js'
|
|
30
|
+
*/
|
|
31
|
+
swDest?: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Disable PWA completely
|
|
35
|
+
* @default false in production, true in development
|
|
36
|
+
* @example disable: process.env.NODE_ENV === 'development'
|
|
37
|
+
*/
|
|
38
|
+
disable?: boolean;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Cache on navigation - cache pages when navigating
|
|
42
|
+
* @default true
|
|
43
|
+
*/
|
|
44
|
+
cacheOnNavigation?: boolean;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Reload app when device goes back online
|
|
48
|
+
* @default true
|
|
49
|
+
*/
|
|
50
|
+
reloadOnOnline?: boolean;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Additional Serwist options
|
|
54
|
+
* @see https://serwist.pages.dev/docs/next/configuring
|
|
55
|
+
*/
|
|
56
|
+
serwistOptions?: Record<string, any>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Add PWA configuration to Next.js config using Serwist
|
|
61
|
+
*
|
|
62
|
+
* @example Basic usage
|
|
63
|
+
* ```ts
|
|
64
|
+
* import { createBaseNextConfig, withPWA } from '@djangocfg/nextjs/config';
|
|
65
|
+
*
|
|
66
|
+
* const nextConfig = createBaseNextConfig({...});
|
|
67
|
+
*
|
|
68
|
+
* export default withPWA(nextConfig, {
|
|
69
|
+
* swSrc: 'app/sw.ts',
|
|
70
|
+
* disable: process.env.NODE_ENV === 'development',
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @example Integrated with createBaseNextConfig
|
|
75
|
+
* ```ts
|
|
76
|
+
* import { createBaseNextConfig } from '@djangocfg/nextjs/config';
|
|
77
|
+
*
|
|
78
|
+
* const config = createBaseNextConfig({
|
|
79
|
+
* pwa: {
|
|
80
|
+
* swSrc: 'app/sw.ts',
|
|
81
|
+
* disable: false,
|
|
82
|
+
* },
|
|
83
|
+
* });
|
|
84
|
+
*
|
|
85
|
+
* export default config;
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function withPWA(
|
|
89
|
+
nextConfig: NextConfig,
|
|
90
|
+
options: PWAPluginOptions = {}
|
|
91
|
+
): NextConfig {
|
|
92
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
93
|
+
|
|
94
|
+
const defaultOptions: PWAPluginOptions = {
|
|
95
|
+
swSrc: 'app/sw.ts',
|
|
96
|
+
swDest: 'public/sw.js',
|
|
97
|
+
disable: options.disable !== undefined ? options.disable : isDev,
|
|
98
|
+
cacheOnNavigation: true,
|
|
99
|
+
reloadOnOnline: true,
|
|
100
|
+
...options,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const withSerwistInit = require('@serwist/next').default;
|
|
105
|
+
|
|
106
|
+
const withSerwist = withSerwistInit({
|
|
107
|
+
swSrc: defaultOptions.swSrc,
|
|
108
|
+
swDest: defaultOptions.swDest,
|
|
109
|
+
disable: defaultOptions.disable,
|
|
110
|
+
cacheOnNavigation: defaultOptions.cacheOnNavigation,
|
|
111
|
+
reloadOnOnline: defaultOptions.reloadOnOnline,
|
|
112
|
+
...defaultOptions.serwistOptions,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return withSerwist(nextConfig);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
consola.error('Failed to configure Serwist:', error);
|
|
118
|
+
return nextConfig;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get service worker template content
|
|
124
|
+
*
|
|
125
|
+
* Returns ready-to-use service worker code for app/sw.ts
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* import { getServiceWorkerTemplate } from '@djangocfg/nextjs/config';
|
|
130
|
+
*
|
|
131
|
+
* // Copy this to your app/sw.ts file
|
|
132
|
+
* console.log(getServiceWorkerTemplate());
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export function getServiceWorkerTemplate(): string {
|
|
136
|
+
return `/**
|
|
137
|
+
* Service Worker (Serwist)
|
|
138
|
+
*
|
|
139
|
+
* Modern PWA service worker using Serwist
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
import { defaultCache } from '@serwist/next/worker';
|
|
143
|
+
import { Serwist } from 'serwist';
|
|
144
|
+
|
|
145
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
146
|
+
declare const self: any;
|
|
147
|
+
|
|
148
|
+
const serwist = new Serwist({
|
|
149
|
+
// Precache entries injected by Serwist build plugin
|
|
150
|
+
precacheEntries: self.__SW_MANIFEST,
|
|
151
|
+
|
|
152
|
+
// Skip waiting - activate new SW immediately
|
|
153
|
+
skipWaiting: true,
|
|
154
|
+
|
|
155
|
+
// Take control of all clients immediately
|
|
156
|
+
clientsClaim: true,
|
|
157
|
+
|
|
158
|
+
// Enable navigation preload for faster loads
|
|
159
|
+
navigationPreload: true,
|
|
160
|
+
|
|
161
|
+
// Use default Next.js runtime caching strategies
|
|
162
|
+
runtimeCaching: defaultCache,
|
|
163
|
+
|
|
164
|
+
// Fallback pages for offline
|
|
165
|
+
fallbacks: {
|
|
166
|
+
entries: [
|
|
167
|
+
{
|
|
168
|
+
url: '/_offline',
|
|
169
|
+
matcher({ request }) {
|
|
170
|
+
return request.destination === 'document';
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
serwist.addEventListeners();
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Backward compatibility exports (deprecated)
|
|
182
|
+
export const defaultRuntimeCaching = [];
|
|
183
|
+
export function createApiCacheRule() {
|
|
184
|
+
consola.warn('createApiCacheRule is deprecated with Serwist. Use defaultCache from @serwist/next/worker');
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
export function createStaticAssetRule() {
|
|
188
|
+
consola.warn('createStaticAssetRule is deprecated with Serwist. Use defaultCache from @serwist/next/worker');
|
|
189
|
+
return {};
|
|
190
|
+
}
|
|
191
|
+
export function createCdnCacheRule() {
|
|
192
|
+
consola.warn('createCdnCacheRule is deprecated with Serwist. Use defaultCache from @serwist/next/worker');
|
|
193
|
+
return {};
|
|
194
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side PWA Utilities
|
|
3
|
+
*
|
|
4
|
+
* Push notifications, VAPID configuration, and route handlers
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // Configure VAPID keys
|
|
9
|
+
* import { configureVapid } from '@djangocfg/nextjs/pwa/server';
|
|
10
|
+
* configureVapid();
|
|
11
|
+
*
|
|
12
|
+
* // Use ready-made route handlers
|
|
13
|
+
* // app/api/push/subscribe/route.ts
|
|
14
|
+
* export { POST, GET } from '@djangocfg/nextjs/pwa/server/routes';
|
|
15
|
+
*
|
|
16
|
+
* // Or use utilities directly
|
|
17
|
+
* import { sendPushNotification } from '@djangocfg/nextjs/pwa/server';
|
|
18
|
+
* await sendPushNotification(subscription, { title: 'Hello!' });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export * from './push';
|
|
23
|
+
export * as routes from './routes';
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side Push Notification Utilities
|
|
3
|
+
*
|
|
4
|
+
* VAPID-based Web Push notifications using web-push library
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import webpush, { PushSubscription } from 'web-push';
|
|
8
|
+
import { consola } from 'consola';
|
|
9
|
+
|
|
10
|
+
let vapidConfigured = false;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if VAPID keys are configured
|
|
14
|
+
*/
|
|
15
|
+
export function isVapidConfigured(): boolean {
|
|
16
|
+
return vapidConfigured;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get VAPID keys from environment
|
|
21
|
+
*/
|
|
22
|
+
export function getVapidKeys() {
|
|
23
|
+
const publicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;
|
|
24
|
+
const privateKey = process.env.VAPID_PRIVATE_KEY;
|
|
25
|
+
const mailto = process.env.VAPID_MAILTO || 'mailto:noreply@example.com';
|
|
26
|
+
|
|
27
|
+
return { publicKey, privateKey, mailto };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Configure VAPID keys for web-push
|
|
32
|
+
* Call this once at app startup
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* // In your API route or middleware
|
|
37
|
+
* import { configureVapid } from '@djangocfg/nextjs/pwa/server';
|
|
38
|
+
*
|
|
39
|
+
* configureVapid(); // Uses env vars automatically
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function configureVapid(options?: {
|
|
43
|
+
publicKey?: string;
|
|
44
|
+
privateKey?: string;
|
|
45
|
+
mailto?: string;
|
|
46
|
+
}): boolean {
|
|
47
|
+
const { publicKey, privateKey, mailto } = options || getVapidKeys();
|
|
48
|
+
|
|
49
|
+
if (!publicKey || !privateKey) {
|
|
50
|
+
consola.warn(
|
|
51
|
+
'⚠️ VAPID keys not configured!\n' +
|
|
52
|
+
' Generate keys: npx web-push generate-vapid-keys\n' +
|
|
53
|
+
' Add to .env.local:\n' +
|
|
54
|
+
' NEXT_PUBLIC_VAPID_PUBLIC_KEY=your_public_key\n' +
|
|
55
|
+
' VAPID_PRIVATE_KEY=your_private_key\n' +
|
|
56
|
+
' Push notifications will not work without VAPID keys.'
|
|
57
|
+
);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
webpush.setVapidDetails(mailto, publicKey, privateKey);
|
|
63
|
+
vapidConfigured = true;
|
|
64
|
+
consola.success('✅ VAPID keys configured for push notifications');
|
|
65
|
+
return true;
|
|
66
|
+
} catch (error: any) {
|
|
67
|
+
consola.error('Failed to configure VAPID keys:', error.message);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Send push notification to a subscription
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* await sendPushNotification(subscription, {
|
|
78
|
+
* title: 'Hello!',
|
|
79
|
+
* body: 'Test notification',
|
|
80
|
+
* data: { url: '/page' },
|
|
81
|
+
* });
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export async function sendPushNotification(
|
|
85
|
+
subscription: PushSubscription,
|
|
86
|
+
notification: {
|
|
87
|
+
title: string;
|
|
88
|
+
body?: string;
|
|
89
|
+
icon?: string;
|
|
90
|
+
badge?: string;
|
|
91
|
+
data?: any;
|
|
92
|
+
tag?: string;
|
|
93
|
+
requireInteraction?: boolean;
|
|
94
|
+
}
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
if (!vapidConfigured) {
|
|
97
|
+
const configured = configureVapid();
|
|
98
|
+
if (!configured) {
|
|
99
|
+
throw new Error('VAPID keys not configured. Cannot send push notification.');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const payload = JSON.stringify({
|
|
104
|
+
title: notification.title,
|
|
105
|
+
body: notification.body || '',
|
|
106
|
+
icon: notification.icon,
|
|
107
|
+
badge: notification.badge,
|
|
108
|
+
data: notification.data,
|
|
109
|
+
tag: notification.tag,
|
|
110
|
+
requireInteraction: notification.requireInteraction,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const result = await webpush.sendNotification(subscription, payload);
|
|
114
|
+
console.log('✅ Push Sent to FCM:', {
|
|
115
|
+
statusCode: result.statusCode,
|
|
116
|
+
headers: result.headers,
|
|
117
|
+
bodyLength: result.body?.length
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Send push notification to multiple subscriptions
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* const results = await sendPushToMultiple(subscriptions, {
|
|
127
|
+
* title: 'Broadcast message',
|
|
128
|
+
* body: 'Sent to all users',
|
|
129
|
+
* });
|
|
130
|
+
* console.log(`Sent: ${results.successful}, Failed: ${results.failed}`);
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export async function sendPushToMultiple(
|
|
134
|
+
subscriptions: PushSubscription[],
|
|
135
|
+
notification: Parameters<typeof sendPushNotification>[1]
|
|
136
|
+
): Promise<{
|
|
137
|
+
successful: number;
|
|
138
|
+
failed: number;
|
|
139
|
+
errors: Array<{ subscription: PushSubscription; error: Error }>;
|
|
140
|
+
}> {
|
|
141
|
+
const results = await Promise.allSettled(
|
|
142
|
+
subscriptions.map((sub) => sendPushNotification(sub, notification))
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const successful = results.filter((r) => r.status === 'fulfilled').length;
|
|
146
|
+
const failed = results.filter((r) => r.status === 'rejected').length;
|
|
147
|
+
const errors = results
|
|
148
|
+
.map((r, i) => (r.status === 'rejected' ? { subscription: subscriptions[i], error: r.reason } : null))
|
|
149
|
+
.filter((e): e is NonNullable<typeof e> => e !== null);
|
|
150
|
+
|
|
151
|
+
return { successful, failed, errors };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Validate push subscription format
|
|
156
|
+
*/
|
|
157
|
+
export function validateSubscription(subscription: any): subscription is PushSubscription {
|
|
158
|
+
return (
|
|
159
|
+
subscription &&
|
|
160
|
+
typeof subscription === 'object' &&
|
|
161
|
+
typeof subscription.endpoint === 'string' &&
|
|
162
|
+
subscription.keys &&
|
|
163
|
+
typeof subscription.keys.p256dh === 'string' &&
|
|
164
|
+
typeof subscription.keys.auth === 'string'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ready-to-use Push Notification Route Handlers
|
|
3
|
+
*
|
|
4
|
+
* Import these in your app/api/push/ routes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
8
|
+
import { configureVapid, sendPushNotification, validateSubscription } from './push';
|
|
9
|
+
|
|
10
|
+
// In-memory storage для demo (в production используй БД)
|
|
11
|
+
const subscriptions = new Set<string>();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* POST /api/push/subscribe
|
|
15
|
+
* Save push subscription
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* // app/api/push/subscribe/route.ts
|
|
20
|
+
* export { POST } from '@djangocfg/nextjs/pwa/server/routes';
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export async function handleSubscribe(request: NextRequest) {
|
|
24
|
+
try {
|
|
25
|
+
const subscription = await request.json();
|
|
26
|
+
|
|
27
|
+
if (!validateSubscription(subscription)) {
|
|
28
|
+
return NextResponse.json(
|
|
29
|
+
{ error: 'Invalid subscription format' },
|
|
30
|
+
{ status: 400 }
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Сохраняем subscription (в demo просто в памяти)
|
|
35
|
+
subscriptions.add(JSON.stringify(subscription));
|
|
36
|
+
|
|
37
|
+
console.log('✅ Push subscription saved:', {
|
|
38
|
+
endpoint: subscription.endpoint.substring(0, 50) + '...',
|
|
39
|
+
total: subscriptions.size,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return NextResponse.json({
|
|
43
|
+
success: true,
|
|
44
|
+
message: 'Subscription saved',
|
|
45
|
+
totalSubscriptions: subscriptions.size,
|
|
46
|
+
});
|
|
47
|
+
} catch (error: any) {
|
|
48
|
+
console.error('Subscription error:', error);
|
|
49
|
+
|
|
50
|
+
return NextResponse.json(
|
|
51
|
+
{
|
|
52
|
+
error: 'Failed to save subscription',
|
|
53
|
+
details: error.message,
|
|
54
|
+
},
|
|
55
|
+
{ status: 500 }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* GET /api/push/subscribe
|
|
62
|
+
* Get all subscriptions (for testing)
|
|
63
|
+
*/
|
|
64
|
+
export async function handleGetSubscriptions() {
|
|
65
|
+
return NextResponse.json({
|
|
66
|
+
totalSubscriptions: subscriptions.size,
|
|
67
|
+
subscriptions: Array.from(subscriptions).map((sub) => {
|
|
68
|
+
const parsed = JSON.parse(sub);
|
|
69
|
+
return {
|
|
70
|
+
endpoint: parsed.endpoint.substring(0, 50) + '...',
|
|
71
|
+
};
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* POST /api/push/send
|
|
78
|
+
* Send push notification
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* // app/api/push/send/route.ts
|
|
83
|
+
* export { POST as handleSend as POST } from '@djangocfg/nextjs/pwa/server/routes';
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export async function handleSend(request: NextRequest) {
|
|
87
|
+
try {
|
|
88
|
+
const { subscription, notification } = await request.json();
|
|
89
|
+
|
|
90
|
+
if (!subscription) {
|
|
91
|
+
return NextResponse.json(
|
|
92
|
+
{ error: 'Subscription is required' },
|
|
93
|
+
{ status: 400 }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!validateSubscription(subscription)) {
|
|
98
|
+
return NextResponse.json(
|
|
99
|
+
{ error: 'Invalid subscription format' },
|
|
100
|
+
{ status: 400 }
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Configure VAPID if not already configured
|
|
105
|
+
configureVapid();
|
|
106
|
+
|
|
107
|
+
await sendPushNotification(subscription, {
|
|
108
|
+
title: notification?.title || 'Test Notification',
|
|
109
|
+
body: notification?.body || 'This is a test push notification',
|
|
110
|
+
icon: notification?.icon,
|
|
111
|
+
badge: notification?.badge,
|
|
112
|
+
data: notification?.data,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return NextResponse.json({
|
|
116
|
+
success: true,
|
|
117
|
+
message: 'Push notification sent',
|
|
118
|
+
});
|
|
119
|
+
} catch (error: any) {
|
|
120
|
+
console.error('Push notification error:', error);
|
|
121
|
+
|
|
122
|
+
return NextResponse.json(
|
|
123
|
+
{
|
|
124
|
+
error: 'Failed to send push notification',
|
|
125
|
+
details: error.message,
|
|
126
|
+
},
|
|
127
|
+
{ status: 500 }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Combined route handlers
|
|
134
|
+
* Use like: export { POST, GET } from '@djangocfg/nextjs/pwa/server/routes'
|
|
135
|
+
*/
|
|
136
|
+
export const POST = handleSubscribe;
|
|
137
|
+
export const GET = handleGetSubscriptions;
|