@asteby/metacore-pwa 0.3.0
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/LICENSE +201 -0
- package/README.md +127 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +5 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/install-prompt.d.ts +20 -0
- package/dist/components/install-prompt.d.ts.map +1 -0
- package/dist/components/install-prompt.js +45 -0
- package/dist/components/install-prompt.js.map +1 -0
- package/dist/components/notification-permission-prompt.d.ts +27 -0
- package/dist/components/notification-permission-prompt.d.ts.map +1 -0
- package/dist/components/notification-permission-prompt.js +84 -0
- package/dist/components/notification-permission-prompt.js.map +1 -0
- package/dist/components/offline-indicator.d.ts +6 -0
- package/dist/components/offline-indicator.d.ts.map +1 -0
- package/dist/components/offline-indicator.js +11 -0
- package/dist/components/offline-indicator.js.map +1 -0
- package/dist/components/update-prompt.d.ts +19 -0
- package/dist/components/update-prompt.d.ts.map +1 -0
- package/dist/components/update-prompt.js +27 -0
- package/dist/components/update-prompt.js.map +1 -0
- package/dist/hooks.d.ts +19 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +42 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/notification-manager.d.ts +29 -0
- package/dist/notification-manager.d.ts.map +1 -0
- package/dist/notification-manager.js +99 -0
- package/dist/notification-manager.js.map +1 -0
- package/dist/provider.d.ts +61 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +176 -0
- package/dist/provider.js.map +1 -0
- package/dist/push-service.d.ts +35 -0
- package/dist/push-service.d.ts.map +1 -0
- package/dist/push-service.js +142 -0
- package/dist/push-service.js.map +1 -0
- package/dist/vite-plugin.d.ts +23 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +83 -0
- package/dist/vite-plugin.js.map +1 -0
- package/package.json +72 -0
- package/templates/sw.js +142 -0
- package/templates/sw.ts +115 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { toast } from 'sonner';
|
|
2
|
+
export class PushNotificationService {
|
|
3
|
+
vapidPublicKey = null;
|
|
4
|
+
subscription = null;
|
|
5
|
+
isSupportedFlag;
|
|
6
|
+
api;
|
|
7
|
+
opts;
|
|
8
|
+
constructor(api, options = {}) {
|
|
9
|
+
this.api = api;
|
|
10
|
+
this.opts = {
|
|
11
|
+
publicKeyPath: options.publicKeyPath ?? '/push/public-key',
|
|
12
|
+
subscribePath: options.subscribePath ?? '/push/subscribe',
|
|
13
|
+
unsubscribePath: options.unsubscribePath ?? '/push/unsubscribe',
|
|
14
|
+
testPath: options.testPath ?? '/push/test',
|
|
15
|
+
};
|
|
16
|
+
this.isSupportedFlag =
|
|
17
|
+
typeof navigator !== 'undefined' &&
|
|
18
|
+
'serviceWorker' in navigator &&
|
|
19
|
+
typeof window !== 'undefined' &&
|
|
20
|
+
'PushManager' in window;
|
|
21
|
+
}
|
|
22
|
+
async init() {
|
|
23
|
+
if (!this.isSupportedFlag) {
|
|
24
|
+
// eslint-disable-next-line no-console
|
|
25
|
+
console.log('Push notifications not supported');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const response = await this.api.get(this.opts.publicKeyPath);
|
|
30
|
+
if (response.data?.publicKey) {
|
|
31
|
+
this.vapidPublicKey = response.data.publicKey;
|
|
32
|
+
}
|
|
33
|
+
const registration = await navigator.serviceWorker.ready;
|
|
34
|
+
this.subscription = await registration.pushManager.getSubscription();
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
console.error('Failed to initialize push service:', error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async subscribe() {
|
|
42
|
+
if (!this.isSupportedFlag) {
|
|
43
|
+
toast.error('Tu navegador no soporta notificaciones push');
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (!this.vapidPublicKey) {
|
|
47
|
+
await this.init();
|
|
48
|
+
if (!this.vapidPublicKey) {
|
|
49
|
+
toast.error('No se pudo obtener la clave del servidor');
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const permission = await Notification.requestPermission();
|
|
55
|
+
if (permission !== 'granted') {
|
|
56
|
+
toast.error('Permiso de notificaciones denegado');
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const registration = await navigator.serviceWorker.ready;
|
|
60
|
+
const subscription = await registration.pushManager.subscribe({
|
|
61
|
+
userVisibleOnly: true,
|
|
62
|
+
applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey),
|
|
63
|
+
});
|
|
64
|
+
this.subscription = subscription;
|
|
65
|
+
const keys = subscription.toJSON().keys;
|
|
66
|
+
await this.api.post(this.opts.subscribePath, {
|
|
67
|
+
endpoint: subscription.endpoint,
|
|
68
|
+
p256dh: keys.p256dh,
|
|
69
|
+
auth: keys.auth,
|
|
70
|
+
device_type: this.detectDeviceType(),
|
|
71
|
+
});
|
|
72
|
+
toast.success('Notificaciones push activadas');
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
// eslint-disable-next-line no-console
|
|
77
|
+
console.error('Failed to subscribe to push:', error);
|
|
78
|
+
toast.error('Error al activar notificaciones push');
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async unsubscribe() {
|
|
83
|
+
if (!this.subscription) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await this.subscription.unsubscribe();
|
|
88
|
+
await this.api.post(this.opts.unsubscribePath, {
|
|
89
|
+
endpoint: this.subscription.endpoint,
|
|
90
|
+
});
|
|
91
|
+
this.subscription = null;
|
|
92
|
+
toast.success('Notificaciones push desactivadas');
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
// eslint-disable-next-line no-console
|
|
97
|
+
console.error('Failed to unsubscribe:', error);
|
|
98
|
+
toast.error('Error al desactivar notificaciones');
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async testNotification() {
|
|
103
|
+
try {
|
|
104
|
+
await this.api.post(this.opts.testPath);
|
|
105
|
+
toast.success('Notificación de prueba enviada');
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
// eslint-disable-next-line no-console
|
|
109
|
+
console.error('Failed to send test notification:', error);
|
|
110
|
+
toast.error('Error al enviar notificación de prueba');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
isSubscribed() {
|
|
114
|
+
return this.subscription !== null;
|
|
115
|
+
}
|
|
116
|
+
getSupported() {
|
|
117
|
+
return this.isSupportedFlag;
|
|
118
|
+
}
|
|
119
|
+
urlBase64ToUint8Array(base64String) {
|
|
120
|
+
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
|
121
|
+
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
122
|
+
const rawData = window.atob(base64);
|
|
123
|
+
const outputArray = new Uint8Array(rawData.length);
|
|
124
|
+
for (let i = 0; i < rawData.length; ++i) {
|
|
125
|
+
outputArray[i] = rawData.charCodeAt(i);
|
|
126
|
+
}
|
|
127
|
+
return outputArray;
|
|
128
|
+
}
|
|
129
|
+
detectDeviceType() {
|
|
130
|
+
if (typeof navigator === 'undefined')
|
|
131
|
+
return 'web';
|
|
132
|
+
const userAgent = navigator.userAgent.toLowerCase();
|
|
133
|
+
if (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent)) {
|
|
134
|
+
return 'mobile';
|
|
135
|
+
}
|
|
136
|
+
if (/electron/i.test(userAgent)) {
|
|
137
|
+
return 'desktop';
|
|
138
|
+
}
|
|
139
|
+
return 'web';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=push-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-service.js","sourceRoot":"","sources":["../src/push-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type VitePWAOptions } from 'vite-plugin-pwa';
|
|
2
|
+
import type { Plugin } from 'vite';
|
|
3
|
+
export interface MetacorePWAOptions extends Partial<VitePWAOptions> {
|
|
4
|
+
/**
|
|
5
|
+
* Regex that matches API URLs to cache with NetworkFirst.
|
|
6
|
+
* Default: `/^https:\/\/api\./i`
|
|
7
|
+
*/
|
|
8
|
+
apiUrlPattern?: RegExp;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Thin wrapper around `vite-plugin-pwa`'s `VitePWA()` with sensible
|
|
12
|
+
* Metacore defaults:
|
|
13
|
+
* - `registerType: 'prompt'`
|
|
14
|
+
* - `strategies: 'injectManifest'` with `srcDir: 'src'`, `filename: 'sw.js'`
|
|
15
|
+
* - Workbox runtime caching for API (NetworkFirst), Google Fonts (SWR + CacheFirst)
|
|
16
|
+
* - Dev mode enabled
|
|
17
|
+
*
|
|
18
|
+
* Any option passed in `options` overrides the defaults. Plain object fields
|
|
19
|
+
* like `manifest` and `workbox` are shallow-merged so callers can override a
|
|
20
|
+
* single key without losing all defaults.
|
|
21
|
+
*/
|
|
22
|
+
export declare function metacorePWA(options?: MetacorePWAOptions): Plugin[];
|
|
23
|
+
//# sourceMappingURL=vite-plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAC9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAElC,MAAM,WAAW,kBAAmB,SAAQ,OAAO,CAAC,cAAc,CAAC;IACjE;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,MAAM,EAAE,CA0EtE"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { VitePWA } from 'vite-plugin-pwa';
|
|
2
|
+
/**
|
|
3
|
+
* Thin wrapper around `vite-plugin-pwa`'s `VitePWA()` with sensible
|
|
4
|
+
* Metacore defaults:
|
|
5
|
+
* - `registerType: 'prompt'`
|
|
6
|
+
* - `strategies: 'injectManifest'` with `srcDir: 'src'`, `filename: 'sw.js'`
|
|
7
|
+
* - Workbox runtime caching for API (NetworkFirst), Google Fonts (SWR + CacheFirst)
|
|
8
|
+
* - Dev mode enabled
|
|
9
|
+
*
|
|
10
|
+
* Any option passed in `options` overrides the defaults. Plain object fields
|
|
11
|
+
* like `manifest` and `workbox` are shallow-merged so callers can override a
|
|
12
|
+
* single key without losing all defaults.
|
|
13
|
+
*/
|
|
14
|
+
export function metacorePWA(options = {}) {
|
|
15
|
+
const { apiUrlPattern = /^https:\/\/api\./i, manifest, workbox, injectManifest, devOptions, ...rest } = options;
|
|
16
|
+
const defaultManifest = {
|
|
17
|
+
name: 'Metacore App',
|
|
18
|
+
short_name: 'Metacore',
|
|
19
|
+
description: 'A Metacore-powered application',
|
|
20
|
+
theme_color: '#84cc16',
|
|
21
|
+
background_color: '#1a2e05',
|
|
22
|
+
start_url: '/',
|
|
23
|
+
display: 'standalone',
|
|
24
|
+
icons: [],
|
|
25
|
+
};
|
|
26
|
+
const defaultWorkbox = {
|
|
27
|
+
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff,woff2}'],
|
|
28
|
+
runtimeCaching: [
|
|
29
|
+
{
|
|
30
|
+
urlPattern: ({ url }) => apiUrlPattern.test(url.href),
|
|
31
|
+
handler: 'NetworkFirst',
|
|
32
|
+
options: {
|
|
33
|
+
cacheName: 'api-cache',
|
|
34
|
+
cacheableResponse: { statuses: [0, 200] },
|
|
35
|
+
expiration: {
|
|
36
|
+
maxEntries: 50,
|
|
37
|
+
maxAgeSeconds: 60 * 60 * 24,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
urlPattern: ({ url }) => /^https:\/\/fonts\.googleapis\.com/.test(url.href),
|
|
43
|
+
handler: 'StaleWhileRevalidate',
|
|
44
|
+
options: {
|
|
45
|
+
cacheName: 'google-fonts-stylesheets',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
urlPattern: ({ url }) => /^https:\/\/fonts\.gstatic\.com/.test(url.href),
|
|
50
|
+
handler: 'CacheFirst',
|
|
51
|
+
options: {
|
|
52
|
+
cacheName: 'google-fonts-webfonts',
|
|
53
|
+
cacheableResponse: { statuses: [0, 200] },
|
|
54
|
+
expiration: {
|
|
55
|
+
maxEntries: 30,
|
|
56
|
+
maxAgeSeconds: 60 * 60 * 24 * 365,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
const defaultInjectManifest = {
|
|
63
|
+
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff,woff2}'],
|
|
64
|
+
};
|
|
65
|
+
const defaultDevOptions = {
|
|
66
|
+
enabled: true,
|
|
67
|
+
type: 'module',
|
|
68
|
+
};
|
|
69
|
+
const merged = {
|
|
70
|
+
strategies: 'injectManifest',
|
|
71
|
+
srcDir: 'src',
|
|
72
|
+
filename: 'sw.ts',
|
|
73
|
+
registerType: 'prompt',
|
|
74
|
+
includeAssets: ['images/**/*'],
|
|
75
|
+
...rest,
|
|
76
|
+
manifest: { ...defaultManifest, ...(manifest ?? {}) },
|
|
77
|
+
workbox: { ...defaultWorkbox, ...(workbox ?? {}) },
|
|
78
|
+
injectManifest: { ...defaultInjectManifest, ...(injectManifest ?? {}) },
|
|
79
|
+
devOptions: { ...defaultDevOptions, ...(devOptions ?? {}) },
|
|
80
|
+
};
|
|
81
|
+
return VitePWA(merged);
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=vite-plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite-plugin.js","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAuB,MAAM,iBAAiB,CAAA;AAW9D;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CAAC,UAA8B,EAAE;IAC1D,MAAM,EAAE,aAAa,GAAG,mBAAmB,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAA;IAE/G,MAAM,eAAe,GAA+B;QAClD,IAAI,EAAE,cAAc;QACpB,UAAU,EAAE,UAAU;QACtB,WAAW,EAAE,gCAAgC;QAC7C,WAAW,EAAE,SAAS;QACtB,gBAAgB,EAAE,SAAS;QAC3B,SAAS,EAAE,GAAG;QACd,OAAO,EAAE,YAAY;QACrB,KAAK,EAAE,EAAE;KACV,CAAA;IAED,MAAM,cAAc,GAA8B;QAChD,YAAY,EAAE,CAAC,2CAA2C,CAAC;QAC3D,cAAc,EAAE;YACd;gBACE,UAAU,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;gBACrD,OAAO,EAAE,cAAc;gBACvB,OAAO,EAAE;oBACP,SAAS,EAAE,WAAW;oBACtB,iBAAiB,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE;oBACzC,UAAU,EAAE;wBACV,UAAU,EAAE,EAAE;wBACd,aAAa,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE;qBAC5B;iBACF;aACF;YACD;gBACE,UAAU,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,mCAAmC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;gBAC3E,OAAO,EAAE,sBAAsB;gBAC/B,OAAO,EAAE;oBACP,SAAS,EAAE,0BAA0B;iBACtC;aACF;YACD;gBACE,UAAU,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,gCAAgC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;gBACxE,OAAO,EAAE,YAAY;gBACrB,OAAO,EAAE;oBACP,SAAS,EAAE,uBAAuB;oBAClC,iBAAiB,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE;oBACzC,UAAU,EAAE;wBACV,UAAU,EAAE,EAAE;wBACd,aAAa,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG;qBAClC;iBACF;aACF;SACF;KACF,CAAA;IAED,MAAM,qBAAqB,GAAqC;QAC9D,YAAY,EAAE,CAAC,2CAA2C,CAAC;KAC5D,CAAA;IAED,MAAM,iBAAiB,GAAiC;QACtD,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,QAAQ;KACf,CAAA;IAED,MAAM,MAAM,GAA4B;QACtC,UAAU,EAAE,gBAAgB;QAC5B,MAAM,EAAE,KAAK;QACb,QAAQ,EAAE,OAAO;QACjB,YAAY,EAAE,QAAQ;QACtB,aAAa,EAAE,CAAC,aAAa,CAAC;QAC9B,GAAG,IAAI;QACP,QAAQ,EAAE,EAAE,GAAG,eAAe,EAAE,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,EAAE;QACrD,OAAO,EAAE,EAAE,GAAG,cAAc,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE;QAClD,cAAc,EAAE,EAAE,GAAG,qBAAqB,EAAE,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC,EAAE;QACvE,UAAU,EAAE,EAAE,GAAG,iBAAiB,EAAE,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE;KAC5D,CAAA;IAED,OAAO,OAAO,CAAC,MAAM,CAAC,CAAA;AACxB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@asteby/metacore-pwa",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Metacore PWA helpers — vite plugin wrapper, install/update prompts, push notifications, offline indicator",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/asteby/metacore-sdk",
|
|
9
|
+
"directory": "packages/pwa"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"templates",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./provider": {
|
|
25
|
+
"types": "./dist/provider.d.ts",
|
|
26
|
+
"import": "./dist/provider.js"
|
|
27
|
+
},
|
|
28
|
+
"./hooks": {
|
|
29
|
+
"types": "./dist/hooks.d.ts",
|
|
30
|
+
"import": "./dist/hooks.js"
|
|
31
|
+
},
|
|
32
|
+
"./components": {
|
|
33
|
+
"types": "./dist/components/index.d.ts",
|
|
34
|
+
"import": "./dist/components/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./vite-plugin": {
|
|
37
|
+
"types": "./dist/vite-plugin.d.ts",
|
|
38
|
+
"import": "./dist/vite-plugin.js"
|
|
39
|
+
},
|
|
40
|
+
"./sw.js": "./templates/sw.js",
|
|
41
|
+
"./sw.ts": "./templates/sw.ts"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": ">=18",
|
|
45
|
+
"react-dom": ">=18",
|
|
46
|
+
"sonner": ">=1.7",
|
|
47
|
+
"vite": ">=5",
|
|
48
|
+
"vite-plugin-pwa": ">=1"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"lucide-react": "^0.460.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/react": "^19.0.0",
|
|
55
|
+
"@types/react-dom": "^19.0.0",
|
|
56
|
+
"react": "^19.2.4",
|
|
57
|
+
"react-dom": "^19.2.4",
|
|
58
|
+
"sonner": "^1.7.0",
|
|
59
|
+
"typescript": "^5.6.0",
|
|
60
|
+
"vite": "^8.0.0",
|
|
61
|
+
"vite-plugin-pwa": "^1.2.0"
|
|
62
|
+
},
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsc -p tsconfig.json",
|
|
68
|
+
"dev": "tsc -w -p tsconfig.json",
|
|
69
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
70
|
+
"clean": "rm -rf dist"
|
|
71
|
+
}
|
|
72
|
+
}
|
package/templates/sw.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metacore PWA service worker template.
|
|
3
|
+
*
|
|
4
|
+
* Copy this file to your app's `src/sw.js` and adjust LANDING_ROUTES /
|
|
5
|
+
* runtime caching patterns to match your deployment.
|
|
6
|
+
*
|
|
7
|
+
* Works with `vite-plugin-pwa` when using `strategies: 'injectManifest'`
|
|
8
|
+
* (which is the default from `@asteby/metacore-pwa/vite-plugin`).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
|
|
12
|
+
import { clientsClaim } from 'workbox-core'
|
|
13
|
+
import { registerRoute } from 'workbox-routing'
|
|
14
|
+
import { NetworkFirst, StaleWhileRevalidate, CacheFirst } from 'workbox-strategies'
|
|
15
|
+
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
|
|
16
|
+
import { ExpirationPlugin } from 'workbox-expiration'
|
|
17
|
+
|
|
18
|
+
// Routes that should bypass the service worker (let your CDN/nginx handle them).
|
|
19
|
+
// Customize this pattern for your deployment.
|
|
20
|
+
const LANDING_ROUTES = /^\/($|en(\/|$)|es(\/|$)|sitemap|robots\.txt|_next\/)/
|
|
21
|
+
|
|
22
|
+
self.addEventListener('fetch', (event) => {
|
|
23
|
+
const url = new URL(event.request.url)
|
|
24
|
+
if (url.origin === self.location.origin && LANDING_ROUTES.test(url.pathname)) {
|
|
25
|
+
// Skip SW — go straight to network
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
cleanupOutdatedCaches()
|
|
31
|
+
|
|
32
|
+
// Precache all assets generated by vite-plugin-pwa
|
|
33
|
+
precacheAndRoute(self.__WB_MANIFEST)
|
|
34
|
+
|
|
35
|
+
self.addEventListener('message', (event) => {
|
|
36
|
+
if (event.data && event.data.type === 'SKIP_WAITING') {
|
|
37
|
+
self.skipWaiting()
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
clientsClaim()
|
|
41
|
+
|
|
42
|
+
// Runtime caching
|
|
43
|
+
|
|
44
|
+
// API
|
|
45
|
+
registerRoute(
|
|
46
|
+
({ url }) => /^https:\/\/api\./i.test(url.href),
|
|
47
|
+
new NetworkFirst({
|
|
48
|
+
cacheName: 'api-cache',
|
|
49
|
+
plugins: [
|
|
50
|
+
new CacheableResponsePlugin({ statuses: [0, 200] }),
|
|
51
|
+
new ExpirationPlugin({
|
|
52
|
+
maxEntries: 50,
|
|
53
|
+
maxAgeSeconds: 60 * 60 * 24,
|
|
54
|
+
}),
|
|
55
|
+
],
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
// Google Fonts stylesheets
|
|
60
|
+
registerRoute(
|
|
61
|
+
({ url }) => /^https:\/\/fonts\.googleapis\.com/.test(url.href),
|
|
62
|
+
new StaleWhileRevalidate({
|
|
63
|
+
cacheName: 'google-fonts-stylesheets',
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
// Google Fonts webfonts
|
|
68
|
+
registerRoute(
|
|
69
|
+
({ url }) => /^https:\/\/fonts\.gstatic\.com/.test(url.href),
|
|
70
|
+
new CacheFirst({
|
|
71
|
+
cacheName: 'google-fonts-webfonts',
|
|
72
|
+
plugins: [
|
|
73
|
+
new CacheableResponsePlugin({ statuses: [0, 200] }),
|
|
74
|
+
new ExpirationPlugin({
|
|
75
|
+
maxEntries: 30,
|
|
76
|
+
maxAgeSeconds: 60 * 60 * 24 * 365,
|
|
77
|
+
}),
|
|
78
|
+
],
|
|
79
|
+
})
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
// Push notifications
|
|
83
|
+
self.addEventListener('push', (event) => {
|
|
84
|
+
let title = 'Notification'
|
|
85
|
+
let body = 'Tienes una nueva notificación'
|
|
86
|
+
let icon = '/images/icons/android/android-launchericon-192-192.png'
|
|
87
|
+
let badge = '/images/icons/monochrome.png'
|
|
88
|
+
let dataUrl = '/'
|
|
89
|
+
let tag = 'general-notification'
|
|
90
|
+
|
|
91
|
+
if (event.data) {
|
|
92
|
+
try {
|
|
93
|
+
const data = event.data.json()
|
|
94
|
+
title = data.title || title
|
|
95
|
+
body = data.body || body
|
|
96
|
+
icon = data.icon || icon
|
|
97
|
+
badge = data.badge || badge
|
|
98
|
+
dataUrl = data.url || dataUrl
|
|
99
|
+
tag = data.tag || tag
|
|
100
|
+
} catch (e) {
|
|
101
|
+
body = event.data.text()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const options = {
|
|
106
|
+
body,
|
|
107
|
+
icon,
|
|
108
|
+
badge,
|
|
109
|
+
data: { url: dataUrl },
|
|
110
|
+
tag,
|
|
111
|
+
vibrate: [100, 50, 100],
|
|
112
|
+
actions: [
|
|
113
|
+
{
|
|
114
|
+
action: 'open',
|
|
115
|
+
title: 'Ver',
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
event.waitUntil(self.registration.showNotification(title, options))
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// Notification click handler
|
|
124
|
+
self.addEventListener('notificationclick', (event) => {
|
|
125
|
+
event.notification.close()
|
|
126
|
+
|
|
127
|
+
event.waitUntil(
|
|
128
|
+
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
|
|
129
|
+
const url = event.notification.data.url
|
|
130
|
+
|
|
131
|
+
for (const client of clientList) {
|
|
132
|
+
if (client.url === url && 'focus' in client) {
|
|
133
|
+
return client.focus()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (clients.openWindow) {
|
|
138
|
+
return clients.openWindow(url)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
)
|
|
142
|
+
})
|
package/templates/sw.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/// <reference lib="webworker" />
|
|
2
|
+
/**
|
|
3
|
+
* Metacore PWA service worker template.
|
|
4
|
+
*
|
|
5
|
+
* Copy to your app's `src/sw.ts` and adjust LANDING_ROUTES / runtime caching
|
|
6
|
+
* patterns to match your deployment.
|
|
7
|
+
*
|
|
8
|
+
* Works with `vite-plugin-pwa` in `injectManifest` mode — which is the default
|
|
9
|
+
* from `@asteby/metacore-pwa/vite-plugin`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { cleanupOutdatedCaches, precacheAndRoute, type PrecacheEntry } from 'workbox-precaching'
|
|
13
|
+
import { clientsClaim } from 'workbox-core'
|
|
14
|
+
import { registerRoute } from 'workbox-routing'
|
|
15
|
+
import { NetworkFirst, StaleWhileRevalidate, CacheFirst } from 'workbox-strategies'
|
|
16
|
+
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
|
|
17
|
+
import { ExpirationPlugin } from 'workbox-expiration'
|
|
18
|
+
|
|
19
|
+
declare const self: ServiceWorkerGlobalScope & {
|
|
20
|
+
__WB_MANIFEST: Array<PrecacheEntry | string>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const LANDING_ROUTES = /^\/($|en(\/|$)|es(\/|$)|sitemap|robots\.txt|_next\/)/
|
|
24
|
+
|
|
25
|
+
self.addEventListener('fetch', (event) => {
|
|
26
|
+
const url = new URL(event.request.url)
|
|
27
|
+
if (url.origin === self.location.origin && LANDING_ROUTES.test(url.pathname)) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
cleanupOutdatedCaches()
|
|
33
|
+
precacheAndRoute(self.__WB_MANIFEST)
|
|
34
|
+
|
|
35
|
+
self.addEventListener('message', (event) => {
|
|
36
|
+
if (event.data && event.data.type === 'SKIP_WAITING') {
|
|
37
|
+
self.skipWaiting()
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
clientsClaim()
|
|
41
|
+
|
|
42
|
+
registerRoute(
|
|
43
|
+
({ url }) => /^https:\/\/api\./i.test(url.href),
|
|
44
|
+
new NetworkFirst({
|
|
45
|
+
cacheName: 'api-cache',
|
|
46
|
+
plugins: [
|
|
47
|
+
new CacheableResponsePlugin({ statuses: [0, 200] }),
|
|
48
|
+
new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 }),
|
|
49
|
+
],
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
registerRoute(
|
|
54
|
+
({ url }) => /^https:\/\/fonts\.googleapis\.com/.test(url.href),
|
|
55
|
+
new StaleWhileRevalidate({ cacheName: 'google-fonts-stylesheets' })
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
registerRoute(
|
|
59
|
+
({ url }) => /^https:\/\/fonts\.gstatic\.com/.test(url.href),
|
|
60
|
+
new CacheFirst({
|
|
61
|
+
cacheName: 'google-fonts-webfonts',
|
|
62
|
+
plugins: [
|
|
63
|
+
new CacheableResponsePlugin({ statuses: [0, 200] }),
|
|
64
|
+
new ExpirationPlugin({ maxEntries: 30, maxAgeSeconds: 60 * 60 * 24 * 365 }),
|
|
65
|
+
],
|
|
66
|
+
})
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
interface PushPayload {
|
|
70
|
+
title?: string
|
|
71
|
+
body?: string
|
|
72
|
+
icon?: string
|
|
73
|
+
badge?: string
|
|
74
|
+
url?: string
|
|
75
|
+
tag?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
self.addEventListener('push', (event) => {
|
|
79
|
+
let payload: PushPayload = {}
|
|
80
|
+
|
|
81
|
+
if (event.data) {
|
|
82
|
+
try {
|
|
83
|
+
payload = event.data.json() as PushPayload
|
|
84
|
+
} catch {
|
|
85
|
+
payload = { body: event.data.text() }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const title = payload.title ?? 'Notification'
|
|
90
|
+
const options: NotificationOptions = {
|
|
91
|
+
body: payload.body ?? 'Tienes una nueva notificación',
|
|
92
|
+
icon: payload.icon ?? '/images/icons/android-launchericon-192-192.png',
|
|
93
|
+
badge: payload.badge ?? '/images/icons/monochrome.png',
|
|
94
|
+
data: { url: payload.url ?? '/' },
|
|
95
|
+
tag: payload.tag ?? 'general-notification',
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
event.waitUntil(self.registration.showNotification(title, options))
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
self.addEventListener('notificationclick', (event) => {
|
|
102
|
+
event.notification.close()
|
|
103
|
+
|
|
104
|
+
event.waitUntil(
|
|
105
|
+
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
|
|
106
|
+
const targetUrl = (event.notification.data as { url?: string } | null)?.url ?? '/'
|
|
107
|
+
for (const client of clientList) {
|
|
108
|
+
if (client.url === targetUrl && 'focus' in client) return client.focus()
|
|
109
|
+
}
|
|
110
|
+
return self.clients.openWindow(targetUrl)
|
|
111
|
+
})
|
|
112
|
+
)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
export {}
|