@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.
Files changed (49) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +127 -0
  3. package/dist/components/index.d.ts +9 -0
  4. package/dist/components/index.d.ts.map +1 -0
  5. package/dist/components/index.js +5 -0
  6. package/dist/components/index.js.map +1 -0
  7. package/dist/components/install-prompt.d.ts +20 -0
  8. package/dist/components/install-prompt.d.ts.map +1 -0
  9. package/dist/components/install-prompt.js +45 -0
  10. package/dist/components/install-prompt.js.map +1 -0
  11. package/dist/components/notification-permission-prompt.d.ts +27 -0
  12. package/dist/components/notification-permission-prompt.d.ts.map +1 -0
  13. package/dist/components/notification-permission-prompt.js +84 -0
  14. package/dist/components/notification-permission-prompt.js.map +1 -0
  15. package/dist/components/offline-indicator.d.ts +6 -0
  16. package/dist/components/offline-indicator.d.ts.map +1 -0
  17. package/dist/components/offline-indicator.js +11 -0
  18. package/dist/components/offline-indicator.js.map +1 -0
  19. package/dist/components/update-prompt.d.ts +19 -0
  20. package/dist/components/update-prompt.d.ts.map +1 -0
  21. package/dist/components/update-prompt.js +27 -0
  22. package/dist/components/update-prompt.js.map +1 -0
  23. package/dist/hooks.d.ts +19 -0
  24. package/dist/hooks.d.ts.map +1 -0
  25. package/dist/hooks.js +42 -0
  26. package/dist/hooks.js.map +1 -0
  27. package/dist/index.d.ts +12 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +7 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/notification-manager.d.ts +29 -0
  32. package/dist/notification-manager.d.ts.map +1 -0
  33. package/dist/notification-manager.js +99 -0
  34. package/dist/notification-manager.js.map +1 -0
  35. package/dist/provider.d.ts +61 -0
  36. package/dist/provider.d.ts.map +1 -0
  37. package/dist/provider.js +176 -0
  38. package/dist/provider.js.map +1 -0
  39. package/dist/push-service.d.ts +35 -0
  40. package/dist/push-service.d.ts.map +1 -0
  41. package/dist/push-service.js +142 -0
  42. package/dist/push-service.js.map +1 -0
  43. package/dist/vite-plugin.d.ts +23 -0
  44. package/dist/vite-plugin.d.ts.map +1 -0
  45. package/dist/vite-plugin.js +83 -0
  46. package/dist/vite-plugin.js.map +1 -0
  47. package/package.json +72 -0
  48. package/templates/sw.js +142 -0
  49. 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
+ }
@@ -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
+ })
@@ -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 {}