diff --git a/public/boltz-icon.svg b/public/boltz-icon.svg index 00801ed2..44846d14 100644 --- a/public/boltz-icon.svg +++ b/public/boltz-icon.svg @@ -1,21 +1,20 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/src/components/BrowserNotification.tsx b/src/components/BrowserNotification.tsx new file mode 100644 index 00000000..4032d955 --- /dev/null +++ b/src/components/BrowserNotification.tsx @@ -0,0 +1,42 @@ +import { useGlobalContext } from "../context/Global"; +import { registerNotifications } from "../utils/notification"; + +const BrowserNotification = () => { + const { browserNotification, setBrowserNotification, t, notify } = + useGlobalContext(); + + const toggle = (evt: MouseEvent) => { + // When disabled, we try to request permission and enable them + if (!browserNotification()) { + registerNotifications().then((state: boolean) => { + setBrowserNotification(state); + if (state === false) { + notify("error", t("browsernotification_error")); + } + }); + evt.stopPropagation(); + return; + } + // When enabled, we disable sending them + setBrowserNotification(false); + evt.stopPropagation(); + }; + + return ( + <> +
+ + {t("on")} + + + {t("off")} + +
+ + ); +}; + +export default BrowserNotification; diff --git a/src/components/RefundButton.tsx b/src/components/RefundButton.tsx index 2f811eaa..780699b0 100644 --- a/src/components/RefundButton.tsx +++ b/src/components/RefundButton.tsx @@ -25,11 +25,11 @@ const RefundButton = ({ setRefundTxId?: Setter; }) => { const { - notify, getSwap, setSwapStorage, setRefundAddress, refundAddress, + notify, t, } = useGlobalContext(); const { setSwap } = usePayContext(); diff --git a/src/components/SettingsMenu.tsx b/src/components/SettingsMenu.tsx index 65c0a7ab..13d753d4 100644 --- a/src/components/SettingsMenu.tsx +++ b/src/components/SettingsMenu.tsx @@ -3,6 +3,7 @@ import { IoClose } from "solid-icons/io"; import { useGlobalContext } from "../context/Global"; import "../style/settings.scss"; import AudioNotificationSetting from "./AudioNotificationSetting"; +import BrowserNotification from "./BrowserNotification"; import Denomination from "./Denomination"; import Logs from "./Logs"; import Separator from "./Separator"; @@ -41,6 +42,12 @@ const SettingsMenu = () => {
+ + + +
+ +
diff --git a/src/components/SwapChecker.tsx b/src/components/SwapChecker.tsx index a8c09498..cee22e62 100644 --- a/src/components/SwapChecker.tsx +++ b/src/components/SwapChecker.tsx @@ -211,7 +211,12 @@ export const SwapChecker = () => { if (claimedSwap.id === swap().id) { setSwap(claimedSwap); } - notify("success", t("claim_success", { id: res.id }), true); + notify( + "success", + t("swap_completed", { id: res.id }), + true, + true, + ); } catch (e) { const msg = t("claim_fail", { id: currentSwap.id }); log.warn(msg, e); @@ -220,11 +225,17 @@ export const SwapChecker = () => { } else if (data.status === swapStatusPending.TransactionClaimPending) { try { await createSubmarineSignature(currentSwap); - } catch (e) { - log.warn( - `creating cooperative signature for submarine swap claim failed`, - e, + notify( + "success", + t("swap_completed", { id: currentSwap.id }), + true, + true, ); + } catch (e) { + const msg = + "creating cooperative signature for submarine swap claim failed"; + log.warn(msg, e); + notify("error", msg); } } }; diff --git a/src/context/Global.tsx b/src/context/Global.tsx index 62b4a428..06736912 100644 --- a/src/context/Global.tsx +++ b/src/context/Global.tsx @@ -63,9 +63,16 @@ export type GlobalContextType = { setSettingsMenu: Setter; audioNotification: Accessor; setAudioNotification: Setter; + browserNotification: Accessor; + setBrowserNotification: Setter; // functions t: (key: string, values?: Record) => string; - notify: (type: string, message: string, audio?: boolean) => void; + notify: ( + type: string, + message: string, + browser?: boolean, + audio?: boolean, + ) => void; playNotificationSound: () => void; fetchPairs: (asset?: string) => void; @@ -171,10 +178,21 @@ const GlobalProvider = (props: { children: any }) => { }, ); - const notify = (type: string, message: string, audio: boolean = false) => { + const notify = ( + type: string, + message: string, + browser: boolean = false, + audio: boolean = false, + ) => { setNotificationType(type); setNotification(message); if (audio && audioNotification()) playNotificationSound(); + if (browser && browserNotification()) { + new Notification(t("notification_header"), { + body: message, + icon: "/boltz-icon.svg", + }); + } }; const playNotificationSound = () => { @@ -294,6 +312,13 @@ const GlobalProvider = (props: { children: any }) => { setEmbedded(true); } + const [browserNotification, setBrowserNotification] = makePersisted( + createSignal(false), + { + name: "browserNotification", + }, + ); + // i18n let dictLocale: any; createMemo(() => setI18n(i18nConfigured())); @@ -345,6 +370,8 @@ const GlobalProvider = (props: { children: any }) => { setSettingsMenu, audioNotification, setAudioNotification, + browserNotification, + setBrowserNotification, // functions t, notify, diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index f96fe8ae..c82e289a 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -61,7 +61,8 @@ const dict = { pay_timeout_blockheight: "Timeout block height", pay_expected_amount: "Expected amount", send_to: "Send {{ amount }} {{ denomination }} to", - pay_invoice_to: "Pay this invoice for {{ amount }} {{ denomination }}", + pay_invoice_to: + "Pay this invoice about {{ amount }} {{ denomination }}", pay_address: "Address", no_metamask: "MetaMask not installed", connect_metamask: "Connect MetaMask", @@ -182,15 +183,19 @@ const dict = { denomination_tooltip: "Choose your preferred denomination: BTC or sats", decimal_tooltip: "Choose your preferred decimal separator: dot or comma", - claim_success: "Swap {{ id }} claimed successfully!", + swap_completed: "Swap {{ id }} completed successfully!", claim_fail: "Failed to claim swap: {{ id }}", logs: "Logs", - logs_tooltip: "Logs of the web app, useful for debugging.", - enable_audio_notifications: "Enable Audio Notifications", + logs_tooltip: "Logs of the web app, useful for debugging", + enable_audio_notifications: "Audio Notifications", enable_audio_notifications_tooltip: "Enable or disable audio notifications", on: "on", off: "off", + notification_header: "Boltz", + browsernotification: "Browser Notifications", + browsernotification_tooltip: "Enable or disable browser notifications", + browsernotification_error: "Notification permissions denied", }, de: { language: "Deutsch", @@ -255,7 +260,8 @@ const dict = { pay_timeout_blockheight: "Timeout Blockhöhe", pay_expected_amount: "Erwarteter Betrag", send_to: "Sende {{ amount }} {{ denomination }} an", - pay_invoice_to: "Zahle Rechnung über {{ amount }} {{ denomination }}", + pay_invoice_to: + "Zahle diese Rechnung über {{ amount }} {{ denomination }}", pay_address: "Adresse", no_metamask: "MetaMask ist nicht installiert", connect_metamask: "MetaMask verbinden", @@ -382,15 +388,20 @@ const dict = { "Wähle deine bevorzugte Denomination: BTC oder sats", decimal_tooltip: "Wähle dein bevorzugtes Dezimaltrennzeichen: Punkt oder Komma", - claim_success: "Swap {{ id }} erfolgreich geclaimed!", + swap_completed: "Swap {{ id }} erfolgreich abgeschlossen!", claim_fail: "Swap {{ id }} konnte nicht geclaimed werden!", logs: "Logs", - logs_tooltip: "Logs der Web App, nützlich für Debugging.", - enable_audio_notifications: "Audio Benachrichtigungen aktivieren", + logs_tooltip: "Logs der Web App, nützlich für Debugging", + enable_audio_notifications: "Audio Benachrichtigungen", enable_audio_notifications_tooltip: "Aktiviere oder deaktiviere Audio-Benachrichtigungen", on: "an", off: "aus", + notification_header: "Boltz", + browsernotification: "Browser Benachrichtigungen", + browsernotification_tooltip: + "Aktiviere oder deaktiviere Browser Benachrichtigungen", + browsernotification_error: "Benachrichtigungsrechte verweigert", }, es: { language: "Español", @@ -582,16 +593,21 @@ const dict = { decimal_separator: "Separador decimal", denomination_tooltip: "Elige tu denominación preferida: BTC o sats", decimal_tooltip: "Elige tu separador decimal preferido: punto o coma", - claim_success: "¡Intercambio {{ id }} reclamado con éxito!", + swap_completed: "¡Intercambio {{ id }} completado con éxito!", claim_fail: "¡Error en reclamar el intercambio {{ id }}!", logs: "Logs", logs_tooltip: - "Registros de la aplicación web como herramienta de depuración.", - enable_audio_notifications: "Activar notificaciones de audio", + "Registros de la aplicación web como herramienta de depuración", + enable_audio_notifications: "Notificaciones de Audio", enable_audio_notifications_tooltip: "Activar o desactivar notificaciones de audio", on: "on", off: "off", + notification_header: "Boltz", + browsernotification: "Notificaciones del navegador", + browsernotification_tooltip: + "Activar o desactivar notificaciones del navegador", + browsernotification_error: "Permisos de notificación denegados", }, zh: { language: "中文", @@ -762,14 +778,18 @@ const dict = { decimal_separator: "小数分隔符", denomination_tooltip: "选择您的首选面额:BTC 或 sats", decimal_tooltip: "选择您的首选小数分隔符:点或逗号", - claim_success: "交换{{ id }}成功索赔!", + swap_completed: "交换{{ id }} 已成功完成!", claim_fail: "交换{{ id }}索赔失败!", logs: "日志", - logs_tooltip: "网络应用程序的日志,用于调试。", - enable_audio_notifications: "启用音频通知", + logs_tooltip: "网络应用程序的日志,用于调试", + enable_audio_notifications: "音频通知", enable_audio_notifications_tooltip: "启用或禁用音频通知", on: "开", off: "关", + notification_header: "Boltz", + browsernotification: "浏览器通知", + browsernotification_tooltip: "启用或禁用浏览器通知", + browsernotification_error: "通知权限被拒绝", }, }; diff --git a/src/style/index.scss b/src/style/index.scss index 7539e988..365e3386 100644 --- a/src/style/index.scss +++ b/src/style/index.scss @@ -344,10 +344,6 @@ textarea { display: block; } -#notification { - background: black; - color: white; -} .toggle { cursor: pointer; flex-grow: 0; @@ -508,7 +504,7 @@ textarea { width: 100%; left: 0; top: 64px; - z-index: 9999; + z-index: 999; display: none; box-shadow: 0 0 12px black; margin: 0; diff --git a/src/style/notification.scss b/src/style/notification.scss index 9acb3d72..643e50cc 100644 --- a/src/style/notification.scss +++ b/src/style/notification.scss @@ -1,5 +1,9 @@ @import "vars"; +#notification { + background: black; + color: white; +} #notification { visibility: hidden; max-width: 480px; @@ -8,7 +12,7 @@ color: #fff; text-align: center; position: fixed; - z-index: 1; + z-index: 9999; left: 0; right: 0; bottom: 30px; diff --git a/src/utils/notification.ts b/src/utils/notification.ts new file mode 100644 index 00000000..f3c8fccb --- /dev/null +++ b/src/utils/notification.ts @@ -0,0 +1,10 @@ +import log from "loglevel"; + +export const registerNotifications = () => { + return new Promise((resolve) => { + Notification.requestPermission().then((result) => { + log.info("Notification permission: ", result); + resolve(result === "granted"); + }); + }); +}; diff --git a/tests/setup.ts b/tests/setup.ts index 8c221bcf..58f57fc7 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -3,3 +3,8 @@ import regtest from "../src/configs/regtest.json"; regtest.loglevel = "error"; setConfig(regtest); + +globalThis.Notification = { + requestPermission: jest.fn().mockResolvedValue(true), + permission: "granted", +} as unknown as jest.Mocked;