+ );
+}
diff --git a/components/pwa-install-prompt.tsx b/components/pwa-install-prompt.tsx
index 4cc95f953..f663f5083 100644
--- a/components/pwa-install-prompt.tsx
+++ b/components/pwa-install-prompt.tsx
@@ -12,6 +12,16 @@ interface BeforeInstallPromptEvent extends Event {
}
const DISMISSED_KEY = "pwa-install-dismissed";
+export const PWA_INSTALL_PROMPT_VISIBILITY_EVENT =
+ "bulwark:pwa-install-prompt-visibility";
+
+// Mirrors the last dispatched visibility so a listener that mounts after the
+// event was fired can still read the current state instead of assuming hidden.
+let promptVisible = false;
+
+export function isPWAInstallPromptVisible(): boolean {
+ return promptVisible;
+}
export function PWAInstallPrompt() {
const [deferredPrompt, setDeferredPrompt] =
@@ -36,16 +46,29 @@ export function PWAInstallPrompt() {
};
}, []);
+ useEffect(() => {
+ promptVisible = showPrompt;
+ window.dispatchEvent(
+ new CustomEvent(PWA_INSTALL_PROMPT_VISIBILITY_EVENT, {
+ detail: { visible: showPrompt },
+ }),
+ );
+ return () => {
+ if (showPrompt) promptVisible = false;
+ };
+ }, [showPrompt]);
+
const handleInstall = async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
- const { outcome } = await deferredPrompt.userChoice;
+ await deferredPrompt.userChoice;
- if (outcome === "accepted") {
- setDeferredPrompt(null);
- setShowPrompt(false);
- }
+ // A beforeinstallprompt event can only be consumed once. Close the card
+ // whether the browser prompt was accepted or dismissed so the next
+ // onboarding step can continue in the same session.
+ setDeferredPrompt(null);
+ setShowPrompt(false);
};
const handleDismiss = () => {
diff --git a/lib/__tests__/dev-jmap-mock.test.ts b/lib/__tests__/dev-jmap-mock.test.ts
index 0de022dd9..9623fee76 100644
--- a/lib/__tests__/dev-jmap-mock.test.ts
+++ b/lib/__tests__/dev-jmap-mock.test.ts
@@ -199,6 +199,81 @@ describe('dev-jmap mock server', () => {
});
});
+ describe('POST /api - PushSubscription lifecycle', () => {
+ it('creates, verifies, lists, and destroys a push subscription', async () => {
+ const relayFetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
+ new Response(null, { status: 202 }),
+ );
+
+ try {
+ const createReq = makeRequest('http://localhost:3000/api/dev-jmap/api', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', host: 'localhost:3000' },
+ body: JSON.stringify({
+ methodCalls: [['PushSubscription/set', {
+ create: {
+ new: {
+ deviceClientId: 'browser-device-1',
+ url: 'https://notifications.relay.bulwarkmail.org/api/push/jmap/browser-device-1',
+ types: ['Email'],
+ expires: '2030-01-01T00:00:00.000Z',
+ },
+ },
+ }, 'c0']],
+ }),
+ });
+ const createRes = await POST(createReq, { params: Promise.resolve({ path: ['api'] }) });
+ const createData = await createRes.json();
+ const id = createData.methodResponses[0][1].created.new.id as string;
+ const verificationBody = JSON.parse(
+ String(relayFetch.mock.calls[0]?.[1]?.body),
+ ) as { verificationCode: string };
+
+ expect(id).toMatch(/^push-/);
+ expect(relayFetch).toHaveBeenCalledOnce();
+
+ const verifyReq = makeRequest('http://localhost:3000/api/dev-jmap/api', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', host: 'localhost:3000' },
+ body: JSON.stringify({
+ methodCalls: [['PushSubscription/set', {
+ update: { [id]: { verificationCode: verificationBody.verificationCode } },
+ }, 'v0']],
+ }),
+ });
+ const verifyRes = await POST(verifyReq, { params: Promise.resolve({ path: ['api'] }) });
+ const verifyData = await verifyRes.json();
+ expect(verifyData.methodResponses[0][1].updated[id]).toBeNull();
+
+ const getReq = makeRequest('http://localhost:3000/api/dev-jmap/api', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', host: 'localhost:3000' },
+ body: JSON.stringify({
+ methodCalls: [['PushSubscription/get', { ids: [id] }, 'g0']],
+ }),
+ });
+ const getRes = await POST(getReq, { params: Promise.resolve({ path: ['api'] }) });
+ const getData = await getRes.json();
+ expect(getData.methodResponses[0][1].list).toEqual([
+ expect.objectContaining({ id, deviceClientId: 'browser-device-1' }),
+ ]);
+
+ const destroyReq = makeRequest('http://localhost:3000/api/dev-jmap/api', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', host: 'localhost:3000' },
+ body: JSON.stringify({
+ methodCalls: [['PushSubscription/set', { destroy: [id] }, 'd0']],
+ }),
+ });
+ const destroyRes = await POST(destroyReq, { params: Promise.resolve({ path: ['api'] }) });
+ const destroyData = await destroyRes.json();
+ expect(destroyData.methodResponses[0][1].destroyed).toEqual([id]);
+ } finally {
+ relayFetch.mockRestore();
+ }
+ });
+ });
+
describe('POST /api - back-references', () => {
it('should resolve #ids from Email/query result', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
diff --git a/locales/ar/common.json b/locales/ar/common.json
index f01446730..134304028 100644
--- a/locales/ar/common.json
+++ b/locales/ar/common.json
@@ -1068,6 +1068,7 @@
"disable": "تعطيل",
"confirm_disable_title": "تعطيل الإشعارات في الخلفية؟",
"confirm_disable_message": "سيتوقف هذا الجهاز عن تلقي التنبيهات عند إغلاق الموقع.",
+ "dismiss_aria": "تجاهل مطالبة الإشعارات",
"ios_hint": "على iOS، ثبّت الموقع على الشاشة الرئيسية أولًا - يقدّم Safari خدمة Web Push فقط لتطبيقات PWA المثبّتة."
},
"sound_selection": {
diff --git a/locales/ca/common.json b/locales/ca/common.json
index b2415c452..50ac94e00 100644
--- a/locales/ca/common.json
+++ b/locales/ca/common.json
@@ -1068,6 +1068,7 @@
"disable": "Desactiva",
"confirm_disable_title": "Voleu desactivar les notificacions en segon pla?",
"confirm_disable_message": "Aquest dispositiu deixarà de rebre avisos quan el lloc estigui tancat.",
+ "dismiss_aria": "Descarta l'avís de notificacions",
"ios_hint": "A l'iOS, primer instal·leu el lloc a la pantalla d'inici - el Safari només entrega Web Push a les PWA instal·lades."
},
"sound_selection": {
diff --git a/locales/cs/common.json b/locales/cs/common.json
index 79aec0196..2e1e6110e 100644
--- a/locales/cs/common.json
+++ b/locales/cs/common.json
@@ -1078,6 +1078,7 @@
"description": "Přijímat systémová oznámení o nové poště, když je tento web zavřený. Doručováno přes push relay Bulwark; relay nikdy nevidí obsah pošty.",
"disable": "Zakázat",
"enable": "Povolit",
+ "dismiss_aria": "Zavřít výzvu k oznámením",
"ios_hint": "Na iOS nejprve nainstalujte web na domovskou obrazovku – Safari doručuje Web Push pouze nainstalovaným PWA.",
"reenable": "Znovu zaregistrovat",
"relay_desc": "Výchozí je hostovaný relay Bulwark. Změňte pouze pokud používáte vlastní hosting.",
diff --git a/locales/da/common.json b/locales/da/common.json
index 04e634e60..0654ac902 100644
--- a/locales/da/common.json
+++ b/locales/da/common.json
@@ -1068,6 +1068,7 @@
"disable": "Deaktivér",
"confirm_disable_title": "Deaktivér baggrundsnotifikationer?",
"confirm_disable_message": "Denne enhed stopper med at modtage alarmer, når sitet er lukket.",
+ "dismiss_aria": "Afvis notifikationsprompt",
"ios_hint": "På iOS skal du installere sitet på din startskærm først - Safari leverer kun Web Push til installerede PWA'er."
},
"sound_selection": {
diff --git a/locales/de/common.json b/locales/de/common.json
index eefe0e83d..d3063628b 100644
--- a/locales/de/common.json
+++ b/locales/de/common.json
@@ -1078,6 +1078,7 @@
"description": "Systembenachrichtigungen für neue E-Mails empfangen, wenn diese Seite geschlossen ist. Zustellung über das Bulwark Push-Relay; das Relay sieht niemals E-Mail-Inhalte.",
"disable": "Deaktivieren",
"enable": "Aktivieren",
+ "dismiss_aria": "Benachrichtigungshinweis schließen",
"ios_hint": "Installieren Sie die Seite unter iOS zuerst auf dem Startbildschirm – Safari liefert Web Push nur an installierte PWAs.",
"reenable": "Neu registrieren",
"relay_desc": "Standardmäßig wird das gehostete Bulwark-Relay verwendet. Nur ändern, wenn Sie selbst hosten.",
diff --git a/locales/en/common.json b/locales/en/common.json
index 6b754b9d7..818714d32 100644
--- a/locales/en/common.json
+++ b/locales/en/common.json
@@ -1067,6 +1067,7 @@
"disable": "Disable",
"confirm_disable_title": "Disable background notifications?",
"confirm_disable_message": "This device will stop receiving alerts when the site is closed.",
+ "dismiss_aria": "Dismiss notification prompt",
"ios_hint": "On iOS, install the site to your home screen first - Safari only delivers Web Push to installed PWAs."
},
"sound_selection": {
diff --git a/locales/es/common.json b/locales/es/common.json
index 812972862..35a76c5b4 100644
--- a/locales/es/common.json
+++ b/locales/es/common.json
@@ -1078,6 +1078,7 @@
"description": "Recibe notificaciones del sistema para correo nuevo cuando este sitio está cerrado. Se entrega a través del relay push de Bulwark; el relay nunca ve el contenido del correo.",
"disable": "Desactivar",
"enable": "Activar",
+ "dismiss_aria": "Cerrar aviso de notificaciones",
"ios_hint": "En iOS, instala primero el sitio en la pantalla de inicio: Safari solo entrega Web Push a PWAs instaladas.",
"reenable": "Volver a registrar",
"relay_desc": "Usa el relay alojado de Bulwark de forma predeterminada. Cámbialo solo si te alojas tú mismo.",
diff --git a/locales/fa/common.json b/locales/fa/common.json
index c46679470..a1d2e4bbf 100644
--- a/locales/fa/common.json
+++ b/locales/fa/common.json
@@ -1068,6 +1068,7 @@
"disable": "غیرفعال کردن",
"confirm_disable_title": "غیرفعال کردن اعلانهای پسزمینه؟",
"confirm_disable_message": "این دستگاه دیگر هشدار دریافت نمیکند",
+ "dismiss_aria": "رد کردن پیشنهاد اعلانها",
"ios_hint": "در iOS ابتدا سایت را به صفحه اصلی اضافه کنید"
},
"sound_selection": {
diff --git a/locales/fr/common.json b/locales/fr/common.json
index 97da02401..338c0690c 100644
--- a/locales/fr/common.json
+++ b/locales/fr/common.json
@@ -1078,6 +1078,7 @@
"description": "Recevez des notifications système pour les nouveaux courriers quand ce site est fermé. Livré via le relais push Bulwark ; le relais ne voit jamais le contenu des courriers.",
"disable": "Désactiver",
"enable": "Activer",
+ "dismiss_aria": "Fermer l'invite de notifications",
"ios_hint": "Sur iOS, installez d'abord le site sur l'écran d'accueil – Safari ne livre Web Push qu'aux PWA installées.",
"reenable": "Réenregistrer",
"relay_desc": "Utilise par défaut le relais Bulwark hébergé. Ne le changez que si vous l'hébergez vous-même.",
diff --git a/locales/he/common.json b/locales/he/common.json
index 4eea36799..63ff27cc3 100644
--- a/locales/he/common.json
+++ b/locales/he/common.json
@@ -1054,6 +1054,7 @@
"disable": "השבת",
"confirm_disable_title": "השבת התראות בדפדפן?",
"confirm_disable_message": "התקן זה יפסיק לקבל התראות כאשר האתר סגור.",
+ "dismiss_aria": "בטל הודעת התראות",
"ios_hint": "ב־iOS, התקן קודם כל את האתר בהיתוך שלך - Safari מספק Web Push רק לPWA מותקנות."
}
},
diff --git a/locales/hu/common.json b/locales/hu/common.json
index dffbb1c38..c143bc085 100644
--- a/locales/hu/common.json
+++ b/locales/hu/common.json
@@ -1068,6 +1068,7 @@
"disable": "Letiltás",
"confirm_disable_title": "Háttérértesítések letiltása?",
"confirm_disable_message": "Ez az eszköz nem fog figyelmeztetéseket kapni, amikor az oldal be van zárva.",
+ "dismiss_aria": "Értesítési ablak elutasítása",
"ios_hint": "iOS-en először telepítsd az oldalt a kezdőképernyőre - a Safari csak telepített PWA-knak kézbesít Web Push értesítéseket."
},
"sound_selection": {
diff --git a/locales/it/common.json b/locales/it/common.json
index d397a69bd..6309480d8 100644
--- a/locales/it/common.json
+++ b/locales/it/common.json
@@ -1078,6 +1078,7 @@
"description": "Ricevi notifiche di sistema per la nuova posta quando questo sito è chiuso. Consegnato tramite il relay push Bulwark; il relay non vede mai il contenuto della posta.",
"disable": "Disabilita",
"enable": "Abilita",
+ "dismiss_aria": "Chiudi avviso di notifiche",
"ios_hint": "Su iOS, installa prima il sito sulla schermata Home: Safari consegna Web Push solo alle PWA installate.",
"reenable": "Registra di nuovo",
"relay_desc": "Per impostazione predefinita usa il relay Bulwark ospitato. Cambialo solo se ospiti in autonomia.",
diff --git a/locales/ja/common.json b/locales/ja/common.json
index 648efffee..0c5890317 100644
--- a/locales/ja/common.json
+++ b/locales/ja/common.json
@@ -1078,6 +1078,7 @@
"description": "このサイトが閉じているときに新着メールのシステム通知を受信します。Bulwark プッシュリレー経由で配信され、リレーがメール内容を見ることはありません。",
"disable": "無効化",
"enable": "有効化",
+ "dismiss_aria": "通知プロンプトを閉じる",
"ios_hint": "iOS では、最初にサイトをホーム画面にインストールしてください。Safari はインストールされた PWA にのみ Web Push を配信します。",
"reenable": "再登録",
"relay_desc": "デフォルトはホストされた Bulwark リレーです。セルフホストする場合のみ変更してください。",
diff --git a/locales/ko/common.json b/locales/ko/common.json
index 38bce8b71..91a642226 100644
--- a/locales/ko/common.json
+++ b/locales/ko/common.json
@@ -1078,6 +1078,7 @@
"description": "이 사이트가 닫혀 있을 때 새 메일에 대한 시스템 알림을 받습니다. Bulwark 푸시 릴레이를 통해 전달되며, 릴레이는 메일 내용을 절대 보지 않습니다.",
"disable": "비활성화",
"enable": "활성화",
+ "dismiss_aria": "알림 프롬프트 닫기",
"ios_hint": "iOS에서는 먼저 사이트를 홈 화면에 설치하세요. Safari는 설치된 PWA에만 Web Push를 전달합니다.",
"reenable": "다시 등록",
"relay_desc": "기본값은 호스팅된 Bulwark 릴레이입니다. 셀프 호스팅 시에만 변경하세요.",
diff --git a/locales/lv/common.json b/locales/lv/common.json
index 1e3f4a86c..650607b80 100644
--- a/locales/lv/common.json
+++ b/locales/lv/common.json
@@ -1078,6 +1078,7 @@
"description": "Saņem sistēmas paziņojumus par jaunu pastu, kad šī vietne ir aizvērta. Piegādāts caur Bulwark push releju; relejs nekad neredz pasta saturu.",
"disable": "Atspējot",
"enable": "Iespējot",
+ "dismiss_aria": "Aizvērt paziņojumu uzaicinājumu",
"ios_hint": "Operētājsistēmā iOS vispirms instalējiet vietni sākuma ekrānā – Safari piegādā Web Push tikai instalētajām PWA.",
"reenable": "Reģistrēt vēlreiz",
"relay_desc": "Pēc noklusējuma izmanto izmitināto Bulwark releju. Mainiet tikai tad, ja izmitināt pats.",
diff --git a/locales/nl/common.json b/locales/nl/common.json
index 59a79db2e..90aee238d 100644
--- a/locales/nl/common.json
+++ b/locales/nl/common.json
@@ -1078,6 +1078,7 @@
"description": "Ontvang systeemmeldingen voor nieuwe e-mail wanneer deze site gesloten is. Geleverd via de Bulwark-pushrelay; de relay ziet nooit e-mailinhoud.",
"disable": "Uitschakelen",
"enable": "Inschakelen",
+ "dismiss_aria": "Meldingsvenster sluiten",
"ios_hint": "Installeer de site op iOS eerst op het beginscherm – Safari levert Web Push alleen aan geïnstalleerde PWA's.",
"reenable": "Opnieuw registreren",
"relay_desc": "Gebruikt standaard de gehoste Bulwark-relay. Wijzig alleen als je zelf hostt.",
diff --git a/locales/pl/common.json b/locales/pl/common.json
index faaca28db..04cde559b 100644
--- a/locales/pl/common.json
+++ b/locales/pl/common.json
@@ -1078,6 +1078,7 @@
"description": "Otrzymuj powiadomienia systemowe o nowych wiadomościach, gdy ta strona jest zamknięta. Dostarczane przez przekaźnik push Bulwark; przekaźnik nigdy nie widzi treści wiadomości.",
"disable": "Wyłącz",
"enable": "Włącz",
+ "dismiss_aria": "Zamknij monit powiadomień",
"ios_hint": "W systemie iOS najpierw zainstaluj stronę na ekranie głównym – Safari dostarcza Web Push tylko zainstalowanym PWA.",
"reenable": "Zarejestruj ponownie",
"relay_desc": "Domyślnie używa hostowanego przekaźnika Bulwark. Zmień tylko jeśli hostujesz samodzielnie.",
diff --git a/locales/pt/common.json b/locales/pt/common.json
index ca4ab329b..9059d7ef8 100644
--- a/locales/pt/common.json
+++ b/locales/pt/common.json
@@ -1078,6 +1078,7 @@
"description": "Receba notificações do sistema para novas mensagens quando este site estiver fechado. Entregue através do relay push do Bulwark; o relay nunca vê o conteúdo da mensagem.",
"disable": "Desativar",
"enable": "Ativar",
+ "dismiss_aria": "Dispensar aviso de notificações",
"ios_hint": "No iOS, instale o site primeiro na tela inicial – o Safari só entrega Web Push para PWAs instalados.",
"reenable": "Registrar novamente",
"relay_desc": "Usa o relay Bulwark hospedado por padrão. Altere apenas se você hospedar.",
diff --git a/locales/ro/common.json b/locales/ro/common.json
index 7da59a55f..488fe569f 100644
--- a/locales/ro/common.json
+++ b/locales/ro/common.json
@@ -1068,6 +1068,7 @@
"disable": "Dezactivare",
"confirm_disable_title": "Dezactivați notificările din fundal?",
"confirm_disable_message": "Acest dispozitiv nu va mai primi alerte atunci când site-ul este închis.",
+ "dismiss_aria": "Ignorați solicitarea de notificări",
"ios_hint": "Pe iOS, instalați mai întâi site-ul pe ecranul de start – Safari trimite notificări Web Push numai către aplicațiile web progresive (PWA) instalate."
},
"sound_selection": {
diff --git a/locales/ru/common.json b/locales/ru/common.json
index 6b7d6440d..6788f7ff4 100644
--- a/locales/ru/common.json
+++ b/locales/ru/common.json
@@ -1078,6 +1078,7 @@
"description": "Получайте системные уведомления о новых письмах, когда этот сайт закрыт. Доставляется через push-релей Bulwark; релей никогда не видит содержимое писем.",
"disable": "Отключить",
"enable": "Включить",
+ "dismiss_aria": "Закрыть запрос на уведомления",
"ios_hint": "В iOS сначала установите сайт на главный экран – Safari доставляет Web Push только установленным PWA.",
"reenable": "Перерегистрировать",
"relay_desc": "По умолчанию используется размещённый релей Bulwark. Меняйте только если хостите самостоятельно.",
diff --git a/locales/sk/common.json b/locales/sk/common.json
index d17133639..a50f1e81a 100644
--- a/locales/sk/common.json
+++ b/locales/sk/common.json
@@ -1092,6 +1092,7 @@
"disable": "Zakázať",
"confirm_disable_title": "Zakázať oznámenia na pozadí?",
"confirm_disable_message": "Toto zariadenie prestane prijímať upozornenia, keď je stránka zatvorená.",
+ "dismiss_aria": "Zavrieť výzvu na oznámenia",
"ios_hint": "Na iOS najprv nainštalujte stránku na domovskú obrazovku – Safari doručuje Web Push iba nainštalovaným PWA."
}
},
diff --git a/locales/tr/common.json b/locales/tr/common.json
index 5764b4279..f90be6c3c 100644
--- a/locales/tr/common.json
+++ b/locales/tr/common.json
@@ -1078,6 +1078,7 @@
"description": "Bu site kapalıyken yeni postalar için sistem bildirimleri alın. Bulwark push röle aracılığıyla iletilir; röle posta içeriğini asla görmez.",
"disable": "Devre dışı bırak",
"enable": "Etkinleştir",
+ "dismiss_aria": "Bildirim istemini kapat",
"ios_hint": "iOS'ta önce siteyi ana ekranınıza yükleyin – Safari Web Push'u yalnızca yüklü PWA'lara teslim eder.",
"reenable": "Yeniden kaydet",
"relay_desc": "Varsayılan olarak barındırılan Bulwark rölesini kullanır. Yalnızca kendiniz barındırıyorsanız değiştirin.",
diff --git a/locales/uk/common.json b/locales/uk/common.json
index 488a43e3e..04fe91c6b 100644
--- a/locales/uk/common.json
+++ b/locales/uk/common.json
@@ -1078,6 +1078,7 @@
"description": "Отримуйте системні сповіщення про нові листи, коли цей сайт закритий. Доставляється через push-реле Bulwark; реле ніколи не бачить вміст листів.",
"disable": "Вимкнути",
"enable": "Увімкнути",
+ "dismiss_aria": "Закрити запит на сповіщення",
"ios_hint": "На iOS спочатку встановіть сайт на головний екран – Safari доставляє Web Push лише встановленим PWA.",
"reenable": "Перереєструвати",
"relay_desc": "Типово використовується розміщене реле Bulwark. Змінюйте лише, якщо хостите самостійно.",
diff --git a/locales/zh/common.json b/locales/zh/common.json
index 288db067d..8194371f6 100644
--- a/locales/zh/common.json
+++ b/locales/zh/common.json
@@ -1078,6 +1078,7 @@
"description": "在此网站关闭时接收新邮件的系统通知。通过 Bulwark 推送中继传送;中继永远不会看到邮件内容。",
"disable": "禁用",
"enable": "启用",
+ "dismiss_aria": "关闭通知提示",
"ios_hint": "在 iOS 上,请先将网站安装到主屏幕 – Safari 仅向已安装的 PWA 发送 Web Push。",
"reenable": "重新注册",
"relay_desc": "默认使用托管的 Bulwark 中继。仅当您自托管时才更改。",