Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 3 additions & 15 deletions frontends/web/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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();
Expand All @@ -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 === '/';
Expand Down Expand Up @@ -173,6 +160,7 @@ export const App = () => {
<ConnectedApp>
<Providers>
<Darkmode />
<IncomingTransactionNotifier activeAccounts={activeAccounts} />
<div className="app">
<AuthRequired/>
<Sidebar
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions frontends/web/src/components/icon/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -132,6 +133,7 @@ export const ArrowUpRed = (props: ImgProps) => (<img src={arrowUpRedSVG} draggab
export const ArrowUTurn = (props: ImgProps) => (<img src={arrowUTurn} draggable={false} {...props} />);
export const ArrowFloorUpRed = (props: ImgProps) => (<img src={arrowFloorUpRedSVG} draggable={false} {...props} />);
export const ArrowFloorDownGreen = (props: ImgProps) => (<img src={arrowFloorDownGreenSVG} draggable={false} {...props} />);
export const ArrowFloorDownBlue = (props: ImgProps) => (<img src={arrowFloorDownBlueSVG} draggable={false} {...props} />);
export const ArrowFloorUpWhite = (props: ImgProps) => (<img src={arrowFloorUpWhiteSVG} draggable={false} {...props} />);
export const ArrowFloorDownWhite = (props: ImgProps) => (<img src={arrowFloorDownWhiteSVG} draggable={false} {...props} />);
export const ArrowCirlceLeft = (props: ImgProps) => (<img src={arrowCircleLeftSVG} draggable={false} {...props} />);
Expand Down
92 changes: 59 additions & 33 deletions frontends/web/src/components/toast/Toast.module.css
Original file line number Diff line number Diff line change
@@ -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;
Comment thread
jadzeidan marked this conversation as resolved.
}

.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;
}
}
.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;
}
}
112 changes: 112 additions & 0 deletions frontends/web/src/components/toast/incoming-transaction-notifier.tsx
Original file line number Diff line number Diff line change
@@ -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<Record<string, boolean>>({});
const initializedAccountsRef = useRef<Record<string, boolean>>({});
const seedingAccountsRef = useRef<Record<string, boolean>>({});

const markIncomingAsSeen = useCallback((accountCode: string, txID: string) => {
shownIncomingTxIDsRef.current[`${accountCode}:${txID}`] = true;
}, []);

const showIncomingTxToast = useCallback((amount: string, unit: string) => {
showToast({
icon: <ArrowFloorDownBlue />,
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;
};
39 changes: 39 additions & 0 deletions frontends/web/src/components/toast/toast-view.tsx
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the old Toast is only used in backups.tsx.

Please remove the deprecated theme prop here and update backups.tsx to use the new toast with type prop.

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 ? <span className={style.icon}>{icon}</span> : undefined;
return (
<Message icon={iconWithSpacing} type={resolvedType} className={`${style.toast || ''} ${className || ''}`.trim()}>
<div className={style.container}>
<div className={style.content}>{children}</div>
<button
aria-label="Close toast"
className={style.closeButton}
hidden={!onClose}
onClick={onClose}
type="button">
{isDarkMode ? <CloseXWhite /> : <CloseXDark />}
</button>
</div>
</Message>
);
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this to components/toast/toast.tsx

Loading
Loading