diff --git a/frontend/index.html b/frontend/index.html
index 440ce1e..69b6847 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -12,6 +12,8 @@
+
+
WealthySmart
diff --git a/frontend/public/manifest.webmanifest b/frontend/public/manifest.webmanifest
new file mode 100644
index 0000000..c2f9e9f
--- /dev/null
+++ b/frontend/public/manifest.webmanifest
@@ -0,0 +1,29 @@
+{
+ "name": "WealthySmart",
+ "short_name": "WealthySmart",
+ "description": "Smart personal finance management",
+ "start_url": "/",
+ "scope": "/",
+ "display": "standalone",
+ "background_color": "#0f172a",
+ "theme_color": "#0f172a",
+ "lang": "es-CR",
+ "icons": [
+ {
+ "src": "/icons/icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/icons/icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ },
+ {
+ "src": "/icons/icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ]
+}
diff --git a/frontend/public/sw.js b/frontend/public/sw.js
index 5c8b7ce..3104549 100644
--- a/frontend/public/sw.js
+++ b/frontend/public/sw.js
@@ -1,4 +1,42 @@
+/* WealthySmart service worker: web push + PWA installability.
+ * (Replaces the old self-unregistering cleanup stub, which also meant
+ * `navigator.serviceWorker.ready` never resolved and push silently no-oped.) */
+
self.addEventListener("install", () => self.skipWaiting());
-self.addEventListener("activate", async () => {
- await self.registration.unregister();
+
+self.addEventListener("activate", (event) => {
+ event.waitUntil(self.clients.claim());
+});
+
+self.addEventListener("push", (event) => {
+ let data = { title: "WealthySmart", body: "", url: "/" };
+ try {
+ data = { ...data, ...event.data.json() };
+ } catch {
+ if (event.data) data.body = event.data.text();
+ }
+ event.waitUntil(
+ self.registration.showNotification(data.title, {
+ body: data.body,
+ icon: "/icons/icon-192.png",
+ badge: "/icons/icon-192.png",
+ data: { url: data.url },
+ }),
+ );
+});
+
+self.addEventListener("notificationclick", (event) => {
+ event.notification.close();
+ const url = event.notification.data?.url || "/";
+ event.waitUntil(
+ self.clients.matchAll({ type: "window", includeUncontrolled: true }).then((clients) => {
+ for (const client of clients) {
+ if ("focus" in client) {
+ client.navigate(url);
+ return client.focus();
+ }
+ }
+ return self.clients.openWindow(url);
+ }),
+ );
});
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index c2a145c..cde6120 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -3,6 +3,13 @@ import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";
+if ("serviceWorker" in navigator) {
+ // Required for both web push and PWA installability.
+ navigator.serviceWorker.register("/sw.js").catch((err) => {
+ console.warn("Service worker registration failed:", err);
+ });
+}
+
createRoot(document.getElementById("root")!).render(