From 6d0e19d547ead1d92700be4de31aa231dc6773fd Mon Sep 17 00:00:00 2001 From: Jad <42831937+jadzeidan@users.noreply.github.com> Date: Tue, 19 May 2026 17:00:09 +0200 Subject: [PATCH] frontend: add toast notifications --- frontends/web/src/app.tsx | 18 +-- .../assets/icons/arrow-floor-down-blue.svg | 3 + frontends/web/src/components/icon/icon.tsx | 2 + .../web/src/components/toast/Toast.module.css | 92 +++++++++----- .../toast/incoming-transaction-notifier.tsx | 112 +++++++++++++++++ .../web/src/components/toast/toast-view.tsx | 39 ++++++ .../web/src/components/toast/toast.test.tsx | 87 +++++++++++++ frontends/web/src/components/toast/toast.tsx | 43 ++++--- frontends/web/src/contexts/index.ts | 5 + frontends/web/src/contexts/providers.tsx | 9 +- frontends/web/src/contexts/toast-context.tsx | 28 +++++ frontends/web/src/contexts/toast-provider.tsx | 99 +++++++++++++++ frontends/web/src/locales/en/app.json | 27 ++++ .../src/routes/device/bitbox02/backups.tsx | 2 +- frontends/web/src/routes/router.tsx | 13 +- .../src/routes/settings/toast-demo.module.css | 34 +++++ .../web/src/routes/settings/toast-demo.tsx | 118 ++++++++++++++++++ 17 files changed, 661 insertions(+), 70 deletions(-) create mode 100644 frontends/web/src/components/icon/assets/icons/arrow-floor-down-blue.svg create mode 100644 frontends/web/src/components/toast/incoming-transaction-notifier.tsx create mode 100644 frontends/web/src/components/toast/toast-view.tsx create mode 100644 frontends/web/src/components/toast/toast.test.tsx create mode 100644 frontends/web/src/contexts/index.ts create mode 100644 frontends/web/src/contexts/toast-context.tsx create mode 100644 frontends/web/src/contexts/toast-provider.tsx create mode 100644 frontends/web/src/routes/settings/toast-demo.module.css create mode 100644 frontends/web/src/routes/settings/toast-demo.tsx diff --git a/frontends/web/src/app.tsx b/frontends/web/src/app.tsx index 488b9bfb34..738cbe7b61 100644 --- a/frontends/web/src/app.tsx +++ b/frontends/web/src/app.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, Fragment } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; +import { getAccounts } from './api/account'; import { useSync } from './hooks/api'; import { useDefault } from './hooks/default'; import { usePrevious } from './hooks/previous'; @@ -11,12 +11,9 @@ import { usePlatformClass } from './hooks/platform'; import { useAppReady } from './hooks/appready'; import { AppRouter } from './routes/router'; import { Wizard as BitBox02Wizard } from './routes/device/bitbox02/wizard'; -import { getAccounts } from './api/account'; import { syncAccountsList } from './api/accountsync'; import { getDeviceList } from './api/devices'; import { syncDeviceList } from './api/devicessync'; -import { syncNewTxs } from './api/transactions'; -import { notifyUser } from './api/system'; import { ConnectedApp } from './connected'; import { Alert } from './components/alert/Alert'; import { Aopp } from './components/aopp/aopp'; @@ -26,15 +23,14 @@ import { Sidebar } from './components/sidebar/sidebar'; import { RouterWatcher } from './utils/route'; import { Darkmode } from './components/darkmode/darkmode'; import { AuthRequired } from './components/auth/authrequired'; +import { IncomingTransactionNotifier } from './components/toast/incoming-transaction-notifier'; import { WCSigningRequest } from './components/wallet-connect/incoming-signing-request'; import { Providers } from './contexts/providers'; import { BottomNavigation } from './components/bottom-navigation/bottom-navigation'; import { getBottomNavKey } from './components/bottom-navigation/utils'; import styles from './app.module.css'; - export const App = () => { usePlatformClass(); - const { t } = useTranslation(); const navigate = useNavigate(); const { pathname } = useLocation(); useIgnoreDrop(); @@ -47,15 +43,6 @@ export const App = () => { const deviceIDs = Object.keys(devices); const firstDevice = deviceIDs[0]; - useEffect(() => { - return syncNewTxs((meta) => { - notifyUser(t('notification.newTxs', { - count: meta.count, - accountName: meta.accountName, - })); - }); - }, [t]); - const maybeRoute = useCallback(() => { const currentURL = window.location.hash.replace(/^#/, ''); const isIndex = currentURL === '' || currentURL === '/'; @@ -173,6 +160,7 @@ export const App = () => { +
+ + diff --git a/frontends/web/src/components/icon/icon.tsx b/frontends/web/src/components/icon/icon.tsx index 17f38c9ebf..db9cda530e 100644 --- a/frontends/web/src/components/icon/icon.tsx +++ b/frontends/web/src/components/icon/icon.tsx @@ -17,6 +17,7 @@ import arrowFloorUpRedSVG from './assets/icons/arrow-floor-up-red.svg'; import arrowFloorDownGreenSVG from './assets/icons/arrow-floor-down-green.svg'; import arrowFloorUpWhiteSVG from './assets/icons/arrow-floor-up-white.svg'; import arrowFloorDownWhiteSVG from './assets/icons/arrow-floor-down-white.svg'; +import arrowFloorDownBlueSVG from './assets/icons/arrow-floor-down-blue.svg'; import arrowCircleLeftSVG from './assets/icons/arrow-circle-left.svg'; import arrowCircleLeftActiveSVG from './assets/icons/arrow-circle-left-active.svg'; import arrowCircleRightSVG from './assets/icons/arrow-circle-right.svg'; @@ -132,6 +133,7 @@ export const ArrowUpRed = (props: ImgProps) => ( (); export const ArrowFloorUpRed = (props: ImgProps) => (); export const ArrowFloorDownGreen = (props: ImgProps) => (); +export const ArrowFloorDownBlue = (props: ImgProps) => (); export const ArrowFloorUpWhite = (props: ImgProps) => (); export const ArrowFloorDownWhite = (props: ImgProps) => (); export const ArrowCirlceLeft = (props: ImgProps) => (); diff --git a/frontends/web/src/components/toast/Toast.module.css b/frontends/web/src/components/toast/Toast.module.css index 48e8a668e2..869b21dc05 100644 --- a/frontends/web/src/components/toast/Toast.module.css +++ b/frontends/web/src/components/toast/Toast.module.css @@ -1,48 +1,74 @@ .toast { - position: fixed; - display: block; - bottom: calc(var(--item-height-large) + var(--space-half)); - right: 50%; - transform: translate(50%, 120%); - max-width: calc(100% - (var(--guide-width) + var(--sidebar-width) + var(--space-default))); - min-width: 180px; - min-height: 20px; - border-radius: 2px; - padding: var(--space-half); - box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.2); - transition: transform ease-out 0.2s; - z-index: 1; + -webkit-backdrop-filter: blur(28px); + backdrop-filter: blur(28px); + margin: 0; } -.active { - transform: translate(50%, 0%); +.toastItem { + animation: toast-in 360ms ease-out; + margin: 0; } -.active.shifted { - right: calc(var(--space-half) + 350px); +.viewport { + display: flex; + flex-direction: column; + gap: var(--space-half); + left: 50%; + max-width: min(560px, calc(100% - var(--space-default) * 2)); + pointer-events: none; + position: fixed; + top: calc(var(--space-default) + env(safe-area-inset-top, 0)); + transform: translateX(-50%); + width: 100%; + z-index: 4005; } -.info { - background-color: var(--color-darkblue); +.viewport > * { + pointer-events: auto; } -.success { - background-color: var(--color-success); +.container { + align-items: flex-start; + display: flex; + width: 100%; } -.warning { - background-color: var(--color-softred); +.content { + margin-right: var(--space-eight); + width: 100%; } -.toast p { - margin: 0; - color: var(--color-alt); +.icon { + display: flex; + margin-right: var(--space-quarter); } -@media (max-width: 768px) { - .toast { - position: initial; - transition: none; - transform: none; - } -} \ No newline at end of file +.closeButton { + background-color: transparent; + border: none; + margin-left: auto; + padding: 0; +} + +.closeButton img { + height: var(--size-default); + margin-right: 0; + width: var(--size-default); +} + +@keyframes toast-in { + from { + transform: translateY(-8px); + } + to { + transform: translateY(0); + } +} + +@media (prefers-reduced-motion: reduce) { + .toastItem, + .closing { + animation: none; + transition: none; + } +} diff --git a/frontends/web/src/components/toast/incoming-transaction-notifier.tsx b/frontends/web/src/components/toast/incoming-transaction-notifier.tsx new file mode 100644 index 0000000000..415b4ba9c7 --- /dev/null +++ b/frontends/web/src/components/toast/incoming-transaction-notifier.tsx @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { useCallback, useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { getTransactionList, TAccount } from '@/api/account'; +import { syncdone } from '@/api/accountsync'; +import { syncNewTxs } from '@/api/transactions'; +import { notifyUser } from '@/api/system'; +import { ArrowFloorDownBlue } from '@/components/icon'; +import { useToast } from '@/contexts/toast-context'; + +type TIncomingTransactionNotifierProps = { + activeAccounts: TAccount[]; +}; + +export const IncomingTransactionNotifier = ({ activeAccounts }: TIncomingTransactionNotifierProps) => { + const { t } = useTranslation(); + const { showToast } = useToast(); + const shownIncomingTxIDsRef = useRef>({}); + const initializedAccountsRef = useRef>({}); + const seedingAccountsRef = useRef>({}); + + const markIncomingAsSeen = useCallback((accountCode: string, txID: string) => { + shownIncomingTxIDsRef.current[`${accountCode}:${txID}`] = true; + }, []); + + const showIncomingTxToast = useCallback((amount: string, unit: string) => { + showToast({ + icon: , + message: t('notification.incomingTxToast', { + amount, + unit, + }), + type: 'info', + }); + }, [showToast, t]); + + const seedIncomingTransactionsForAccount = useCallback(async (account: TAccount) => { + if (initializedAccountsRef.current[account.code] || seedingAccountsRef.current[account.code]) { + return; + } + + seedingAccountsRef.current[account.code] = true; + try { + const transactions = await getTransactionList(account.code); + if (!transactions.success) { + return; + } + transactions.list + .filter(tx => tx.type === 'receive') + .forEach(tx => markIncomingAsSeen(account.code, tx.internalID)); + initializedAccountsRef.current[account.code] = true; + } finally { + seedingAccountsRef.current[account.code] = false; + } + }, [markIncomingAsSeen]); + + // Seed known incoming transactions for active accounts so we only toast truly new ones. + useEffect(() => { + void Promise.all(activeAccounts.map(account => seedIncomingTransactionsForAccount(account))); + }, [activeAccounts, seedIncomingTransactionsForAccount]); + + useEffect(() => { + return syncNewTxs((meta) => { + notifyUser(t('notification.newTxs', { + count: meta.count, + accountName: meta.accountName, + })); + }); + }, [t]); + + const detectAndToastIncomingForAccount = useCallback(async (account: TAccount) => { + if (!initializedAccountsRef.current[account.code]) { + return; + } + + const transactions = await getTransactionList(account.code); + if (!transactions.success) { + return; + } + + // New transactions are sorted to the front. Check only the latest window. + const recentTransactions = transactions.list.slice(0, 30); + recentTransactions + .filter(tx => tx.type === 'receive') + .reverse() + .forEach((tx) => { + const txKey = `${account.code}:${tx.internalID}`; + if (shownIncomingTxIDsRef.current[txKey]) { + return; + } + markIncomingAsSeen(account.code, tx.internalID); + showIncomingTxToast(tx.amount.amount, tx.amount.unit); + }); + }, [markIncomingAsSeen, showIncomingTxToast]); + + useEffect(() => { + const unsubscribers = activeAccounts.map((account) => { + return syncdone(account.code, () => { + void (async () => { + await seedIncomingTransactionsForAccount(account); + await detectAndToastIncomingForAccount(account); + })(); + }); + }); + return () => { + unsubscribers.forEach(unsubscribe => unsubscribe()); + }; + }, [activeAccounts, detectAndToastIncomingForAccount, seedIncomingTransactionsForAccount]); + + return null; +}; diff --git a/frontends/web/src/components/toast/toast-view.tsx b/frontends/web/src/components/toast/toast-view.tsx new file mode 100644 index 0000000000..a9a43c3cb5 --- /dev/null +++ b/frontends/web/src/components/toast/toast-view.tsx @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { ReactNode } from 'react'; +import { CloseXDark, CloseXWhite } from '@/components/icon'; +import { Message } from '@/components/message/message'; +import { useDarkmode } from '@/hooks/darkmode'; +import { TMessageTypes } from '@/utils/types'; +import style from './Toast.module.css'; + +type TToastProps = { + type?: TMessageTypes; + // Deprecated prop kept for compatibility with the existing callsites. + theme?: TMessageTypes; + icon?: ReactNode; + className?: string; + onClose?: () => void; + children: ReactNode; +}; + +export const Toast = ({ type, theme, icon, className = '', onClose, children }: TToastProps) => { + const resolvedType = type || theme || 'info'; + const { isDarkMode } = useDarkmode(); + const iconWithSpacing = icon ? {icon} : undefined; + return ( + +
+
{children}
+ +
+
+ ); +}; diff --git a/frontends/web/src/components/toast/toast.test.tsx b/frontends/web/src/components/toast/toast.test.tsx new file mode 100644 index 0000000000..9956502636 --- /dev/null +++ b/frontends/web/src/components/toast/toast.test.tsx @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useToast } from '@/contexts/toast-context'; +import { ToastProvider } from '@/contexts/toast-provider'; + +const TestToastTrigger = () => { + const { clearToasts, showToast } = useToast(); + return ( + <> + + + + + ); +}; + +describe('components/toast/toast', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('shows and auto-dismisses a toast', () => { + render( + + + + ); + + fireEvent.click(screen.getByRole('button', { name: 'Trigger toast' })); + + expect(screen.getByText('Toast message')).toBeInTheDocument(); + + act(() => { + vi.advanceTimersByTime(2100); + }); + + expect(screen.queryByText('Toast message')).not.toBeInTheDocument(); + }); + + it('clears all visible toasts', () => { + render( + + + + ); + + fireEvent.click(screen.getByRole('button', { name: 'Trigger toast' })); + + expect(screen.getByText('Toast message')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: 'Clear toasts' })); + + expect(screen.queryByText('Toast message')).not.toBeInTheDocument(); + }); + + it('keeps a persistent toast visible until dismissed', () => { + render( + + + + ); + + fireEvent.click(screen.getByRole('button', { name: 'Trigger persistent toast' })); + + act(() => { + vi.advanceTimersByTime(15000); + }); + + expect(screen.getByText('Persistent toast')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: 'Close toast' })); + + expect(screen.queryByText('Persistent toast')).not.toBeInTheDocument(); + }); +}); diff --git a/frontends/web/src/components/toast/toast.tsx b/frontends/web/src/components/toast/toast.tsx index 0bbefd37d7..e92cc03411 100644 --- a/frontends/web/src/components/toast/toast.tsx +++ b/frontends/web/src/components/toast/toast.tsx @@ -1,27 +1,36 @@ // SPDX-License-Identifier: Apache-2.0 -import { ReactNode, useEffect, useState } from 'react'; +import { ReactNode } from 'react'; +import { CloseXDark, CloseXWhite } from '@/components/icon'; +import { Message } from '@/components/message/message'; +import { useDarkmode } from '@/hooks/darkmode'; +import { TMessageTypes } from '@/utils/types'; import style from './Toast.module.css'; -type TProps = { - theme: string; - withGuide?: boolean; +type TToastProps = { + type?: TMessageTypes; + icon?: ReactNode; + onClose?: () => void; children: ReactNode; }; -export const Toast = ({ theme, withGuide = false, children }: TProps) => { - const [active, setActive] = useState(false); - - useEffect(() => { - setTimeout(() => setActive(true), 5); - }, []); - +export const Toast = ({ type = 'info', icon, onClose, children }: TToastProps) => { + const { isDarkMode } = useDarkmode(); + const iconWithSpacing = icon ? {icon} : undefined; + const className = [style.toast, style.toastItem].filter(Boolean).join(' '); return ( -
-

{children}

-
+ +
+
{children}
+ +
+
); }; - - diff --git a/frontends/web/src/contexts/index.ts b/frontends/web/src/contexts/index.ts new file mode 100644 index 0000000000..dd1379631e --- /dev/null +++ b/frontends/web/src/contexts/index.ts @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + +export { ToastContext, useToast } from './toast-context'; +export { ToastProvider } from './toast-provider'; +export type { TShowToast, TToastContext } from './toast-context'; diff --git a/frontends/web/src/contexts/providers.tsx b/frontends/web/src/contexts/providers.tsx index d2e453076a..b744cac5fa 100644 --- a/frontends/web/src/contexts/providers.tsx +++ b/frontends/web/src/contexts/providers.tsx @@ -8,6 +8,7 @@ import { BackNavigationProvider } from './BackNavigationContext'; import { WCWeb3WalletProvider } from './WCWeb3WalletProvider'; import { RatesProvider } from './RatesProvider'; import { LocalizationProvider } from './localization-provider'; +import { ToastProvider } from './toast-provider'; type Props = { children: ReactNode; @@ -21,9 +22,11 @@ export const Providers = ({ children }: Props) => { - - {children} - + + + {children} + + diff --git a/frontends/web/src/contexts/toast-context.tsx b/frontends/web/src/contexts/toast-context.tsx new file mode 100644 index 0000000000..4daf01e32e --- /dev/null +++ b/frontends/web/src/contexts/toast-context.tsx @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { ReactNode, createContext, useContext } from 'react'; +import { TMessageTypes } from '@/utils/types'; + +export type TShowToast = { + duration?: number; + icon?: ReactNode; + message: ReactNode; + persistent?: boolean; + type?: TMessageTypes; +}; + +export type TToastContext = { + clearToasts: () => void; + hideToast: (id: number) => void; + showToast: (toast: TShowToast) => number; +}; + +export const ToastContext = createContext(undefined); + +export const useToast = () => { + const context = useContext(ToastContext); + if (!context) { + throw new Error('useToast must be used within ToastProvider.'); + } + return context; +}; diff --git a/frontends/web/src/contexts/toast-provider.tsx b/frontends/web/src/contexts/toast-provider.tsx new file mode 100644 index 0000000000..01bd6e3ae3 --- /dev/null +++ b/frontends/web/src/contexts/toast-provider.tsx @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { TMessageTypes } from '@/utils/types'; +import { Toast } from '@/components/toast/toast'; +import { ToastContext, TShowToast } from './toast-context'; +import style from '@/components/toast/Toast.module.css'; + +type TToastItem = { + id: number; + duration: number; + icon?: ReactNode; + message: ReactNode; + persistent: boolean; + type: TMessageTypes; +}; + +type TToastProviderProps = { + children: ReactNode; +}; + +const DEFAULT_DURATION_MS = 5000; + +let nextToastID = 1; + +export const ToastProvider = ({ children }: TToastProviderProps) => { + const [toasts, setToasts] = useState([]); + const timeoutIDs = useRef>>({}); + + const hideToast = useCallback((id: number) => { + const timeoutID = timeoutIDs.current[id]; + if (timeoutID) { + clearTimeout(timeoutID); + delete timeoutIDs.current[id]; + } + setToasts(prevToasts => prevToasts.filter(toast => toast.id !== id)); + }, []); + + const clearToasts = useCallback(() => { + Object.values(timeoutIDs.current).forEach(clearTimeout); + timeoutIDs.current = {}; + setToasts([]); + }, []); + + const showToast = useCallback((toast: TShowToast) => { + const id = nextToastID; + nextToastID += 1; + const persistent = toast.persistent === true; + + const nextToast: TToastItem = { + duration: persistent ? 0 : (toast.duration ?? DEFAULT_DURATION_MS), + id, + icon: toast.icon, + message: toast.message, + persistent, + type: toast.type ?? 'info', + }; + + setToasts(prevToasts => [...prevToasts, nextToast]); + + if (nextToast.duration > 0) { + timeoutIDs.current[id] = setTimeout(() => { + hideToast(id); + }, nextToast.duration); + } + return id; + }, [hideToast]); + + useEffect(() => { + return () => { + Object.values(timeoutIDs.current).forEach(clearTimeout); + }; + }, []); + + const value = useMemo(() => ({ + clearToasts, + hideToast, + showToast, + }), [clearToasts, hideToast, showToast]); + + return ( + + {children} + {toasts.length > 0 && ( +
+ {toasts.map(({ id, icon, message, persistent, type }) => ( + hideToast(id) : undefined} + type={type}> + {message} + + ))} +
+ )} +
+ ); +}; diff --git a/frontends/web/src/locales/en/app.json b/frontends/web/src/locales/en/app.json index 84a39d72c2..e2ad3d81fd 100644 --- a/frontends/web/src/locales/en/app.json +++ b/frontends/web/src/locales/en/app.json @@ -1634,6 +1634,7 @@ "title": "Note" }, "notification": { + "incomingTxToast": "Incoming {{amount}} {{unit}}", "newTxs_one": "New transaction in: {{accountName}}", "newTxs_other": "{{count}} new transactions in: {{accountName}}" }, @@ -2052,6 +2053,32 @@ "title": "Exit testnet mode" } }, + "toastDemo": { + "buttons": { + "bitbox02Nova": "Show BitBox02 Nova toast", + "error": "Show error toast", + "info": "Show info toast", + "incoming": "Show incoming BTC toast", + "long": "Show long toast", + "persistent": "Show persistent toast", + "success": "Show success toast", + "warning": "Show warning toast" + }, + "description": "Trigger a toast to preview the banner-based toast component.", + "examples": { + "bitbox02Nova": { + "action": "Set up now", + "prefix": "A new BitBox02 Nova has been found." + }, + "error": "Something went wrong. Please try again.", + "info": "Heads up! This is an informational toast.", + "incomingBtc": "Incoming 0.01 BTC", + "persistent": "This toast will stay here until you dismiss it.", + "success": "Success! The action completed.", + "warning": "Warning: Please double-check before continuing." + }, + "title": "Toast demo" + }, "transaction": { "advanced": "Advanced", "confirmation": "Confirmations", diff --git a/frontends/web/src/routes/device/bitbox02/backups.tsx b/frontends/web/src/routes/device/bitbox02/backups.tsx index 5c5daf1502..5e7892773e 100644 --- a/frontends/web/src/routes/device/bitbox02/backups.tsx +++ b/frontends/web/src/routes/device/bitbox02/backups.tsx @@ -100,7 +100,7 @@ export const BackupsV2 = ({
{ errorText && ( - + {errorText} ) diff --git a/frontends/web/src/routes/router.tsx b/frontends/web/src/routes/router.tsx index db58213964..28d054186b 100644 --- a/frontends/web/src/routes/router.tsx +++ b/frontends/web/src/routes/router.tsx @@ -1,9 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 -import React, { ReactChild } from 'react'; +import React, { ReactChild, useContext } from 'react'; import { Route, Routes, useParams } from 'react-router-dom'; import { TAccount } from '@/api/account'; import { TDevices } from '@/api/devices'; +import { AppContext } from '@/contexts/AppContext'; import { AddAccount } from './account/add/add-account'; import { Moonpay } from './market/moonpay'; import { Market } from './market/market'; @@ -42,6 +43,7 @@ import { ConnectScreenWalletConnect } from './account/walletconnect/connect'; import { DashboardWalletConnect } from './account/walletconnect/dashboard'; import { AllAccounts } from '@/routes/accounts/all-accounts'; import { More } from '@/routes/settings/more'; +import { ToastDemo } from '@/routes/settings/toast-demo'; type TAppRouterProps = { devices: TDevices; @@ -60,6 +62,7 @@ const InjectParams = ({ children }: TInjectParamsProps) => { }; export const AppRouter = ({ devices, devicesKey, accounts, activeAccounts }: TAppRouterProps) => { + const { isDevServers } = useContext(AppContext); const hasAccounts = accounts.length > 0; const Homepage = ( ); + const ToastDemoEl = ( + + ); + const AdvancedSettingsEl = ( + {isDevServers && } } /> ( + + + +); + +export const ToastDemo = ({ devices, hasAccounts }: TPagePropsWithSettingsTabs) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { showToast } = useToast(); + + const show = (type: TMessageTypes, duration?: number) => { + showToast({ + duration, + message: t(`toastDemo.examples.${type}`), + type, + }); + }; + + return ( + + +
+ + + +
+

{t('sidebar.settings')}

+ + + } /> + + + +

{t('toastDemo.description')}

+
+ + + + + + + + + + ), + type: 'info', + }); + }}> + {t('toastDemo.buttons.bitbox02Nova')} + +
+
+
+
+
+
+
+ ); +};