From 401ed99f0abdfe565227cc3a5c5e0da8431dca1e Mon Sep 17 00:00:00 2001 From: Fara Woolf Date: Tue, 5 Mar 2024 13:01:14 -0600 Subject: [PATCH] feat: toast --- package.json | 1 + src/app/app.tsx | 28 +++--- .../hooks/account/use-create-account.ts | 6 +- .../hooks/use-submit-stx-transaction.ts | 4 +- src/app/debug.ts | 5 +- src/app/features/container/container.tsx | 2 - src/app/features/html-head/head-provider.tsx | 6 +- .../components/increase-stx-fee-form.tsx | 7 +- .../hooks/use-btc-increase-fee.ts | 3 +- .../ledger-bitcoin-sign-tx-container.tsx | 3 +- .../ledger-request-stacks-keys.tsx | 3 +- .../retrieve-taproot-to-native-segwit.tsx | 3 +- .../components/advanced-menu-items.tsx | 3 +- .../use-stacks-broadcast-transaction.tsx | 14 +-- src/app/features/toasts/toasts.tsx | 58 ++++++++++++ src/app/features/toasts/use-toast-handlers.ts | 51 ++++++++++ src/app/features/toasts/use-toast.ts | 15 +++ src/app/pages/receive/receive-btc.tsx | 3 +- src/app/pages/receive/receive-modal.tsx | 3 +- src/app/pages/receive/receive-ordinal.tsx | 3 +- src/app/pages/receive/receive-stx.tsx | 4 +- .../rpc-send-transfer-summary.tsx | 3 +- .../use-sign-bip322-message.ts | 3 +- .../rpc-sign-psbt/rpc-sign-psbt-summary.tsx | 3 +- .../choose-crypto-asset.tsx | 3 +- .../locked-bitcoin-summary.tsx | 3 +- .../sent-inscription-summary.tsx | 4 +- .../components/send-max-button.tsx | 10 +- .../family/bitcoin/hooks/use-send-max.tsx | 12 ++- .../form/brc-20/brc-20-choose-fee.tsx | 3 +- .../stacks-sip10/sip10-token-send-form.tsx | 5 +- .../send/sent-summary/btc-sent-summary.tsx | 5 +- .../send/sent-summary/stx-sent-summary.tsx | 5 +- .../swap/hooks/use-stacks-broadcast-swap.tsx | 5 +- .../balance/stacks-ft-balances.hooks.ts | 6 +- .../store/transactions/transaction.hooks.ts | 6 +- src/app/ui/components/toast/toast.layout.tsx | 15 +++ src/app/ui/components/toast/toast.stories.tsx | 92 +++++++++++++++++++ src/app/ui/components/toast/toast.tsx | 60 ++++++++++++ src/app/ui/components/toast/toast.utils.tsx | 16 ++++ theme/keyframes.ts | 4 + yarn.lock | 19 ++++ 42 files changed, 437 insertions(+), 70 deletions(-) create mode 100644 src/app/features/toasts/toasts.tsx create mode 100644 src/app/features/toasts/use-toast-handlers.ts create mode 100644 src/app/features/toasts/use-toast.ts create mode 100644 src/app/ui/components/toast/toast.layout.tsx create mode 100644 src/app/ui/components/toast/toast.stories.tsx create mode 100644 src/app/ui/components/toast/toast.tsx create mode 100644 src/app/ui/components/toast/toast.utils.tsx diff --git a/package.json b/package.json index 97cf8396508..65274a18027 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "@radix-ui/react-dropdown-menu": "2.0.6", "@radix-ui/react-select": "2.0.0", "@radix-ui/react-tabs": "1.0.4", + "@radix-ui/react-toast": "1.1.5", "@radix-ui/react-tooltip": "1.0.7", "@radix-ui/themes": "2.0.3", "@reduxjs/toolkit": "1.9.6", diff --git a/src/app/app.tsx b/src/app/app.tsx index ac9526927ad..d3ed5a2bc17 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -1,20 +1,22 @@ import { Suspense } from 'react'; import { Provider as ReduxProvider } from 'react-redux'; +// import { Provider as RadixToastProvider } from '@radix-ui/react-toast'; import { radixBaseCSS } from '@radix-ui/themes/styles.css'; import { QueryClientProvider } from '@tanstack/react-query'; import { styled } from 'leather-styles/jsx'; import { PersistGate } from 'redux-persist/integration/react'; import { queryClient } from '@app/common/persistence'; +import { ThemeSwitcherProvider } from '@app/common/theme-provider'; import { FullPageLoadingSpinner } from '@app/components/loading-spinner'; import { Devtools } from '@app/features/devtool/devtools'; import { AppErrorBoundary } from '@app/features/errors/app-error-boundary'; +import { HeadProvider } from '@app/features/html-head/head-provider'; +import { Toasts } from '@app/features/toasts/toasts'; import { AppRoutes } from '@app/routes/app-routes'; import { persistor, store } from '@app/store'; -import { ThemeSwitcherProvider } from './common/theme-provider'; -import { HeadProvider } from './features/html-head/head-provider'; import './index.css'; const reactQueryDevToolsEnabled = process.env.REACT_QUERY_DEVTOOLS_ENABLED === 'true'; @@ -26,16 +28,18 @@ export function App() { {/* TODO: this works but investigate importing radixBaseCSS in panda layer config */} - - - }> - - - - {reactQueryDevToolsEnabled && } - - - + + + + }> + + + + {reactQueryDevToolsEnabled && } + + + + diff --git a/src/app/common/hooks/account/use-create-account.ts b/src/app/common/hooks/account/use-create-account.ts index a98011d481e..078e1f63056 100644 --- a/src/app/common/hooks/account/use-create-account.ts +++ b/src/app/common/hooks/account/use-create-account.ts @@ -1,19 +1,19 @@ import { useCallback } from 'react'; -import { toast } from 'react-hot-toast'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { useKeyActions } from '@app/common/hooks/use-key-actions'; +import { useToast } from '@app/features/toasts/use-toast'; export function useCreateAccount() { const { createNewAccount } = useKeyActions(); const analytics = useAnalytics(); + const toast = useToast(); return useCallback(() => { void analytics.track('create_new_account'); void toast.promise(createNewAccount(), { - loading: 'Creating account...', success: 'Account created!', error: 'Error creating account.', }); - }, [analytics, createNewAccount]); + }, [analytics, createNewAccount, toast]); } diff --git a/src/app/common/hooks/use-submit-stx-transaction.ts b/src/app/common/hooks/use-submit-stx-transaction.ts index 7f64bd24286..0f97612f916 100644 --- a/src/app/common/hooks/use-submit-stx-transaction.ts +++ b/src/app/common/hooks/use-submit-stx-transaction.ts @@ -1,5 +1,4 @@ import { useCallback } from 'react'; -import { toast } from 'react-hot-toast'; import { bytesToHex } from '@stacks/common'; import { StacksTransaction, broadcastTransaction } from '@stacks/transactions'; @@ -12,6 +11,7 @@ import { useRefreshAllAccountData } from '@app/common/hooks/account/use-refresh- import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { useLoading } from '@app/common/hooks/use-loading'; import { safelyFormatHexTxid } from '@app/common/utils/safe-handle-txid'; +import { useToast } from '@app/features/toasts/use-toast'; import { useCurrentStacksNetworkState } from '@app/store/networks/networks.hooks'; import { useSubmittedTransactionsActions } from '@app/store/submitted-transactions/submitted-transactions.hooks'; @@ -27,6 +27,7 @@ interface UseSubmitTransactionCallbackArgs { } export function useSubmitTransactionCallback({ loadingKey }: UseSubmitTransactionArgs) { const submittedTransactionsActions = useSubmittedTransactionsActions(); + const toast = useToast(); const analytics = useAnalytics(); const refreshAccountData = useRefreshAllAccountData(); @@ -67,6 +68,7 @@ export function useSubmitTransactionCallback({ loadingKey }: UseSubmitTransactio [ setIsLoading, stacksNetwork, + toast, setIsIdle, submittedTransactionsActions, analytics, diff --git a/src/app/debug.ts b/src/app/debug.ts index 04ef5d35621..bbd57b9d427 100644 --- a/src/app/debug.ts +++ b/src/app/debug.ts @@ -1,7 +1,6 @@ -import toast from 'react-hot-toast'; - import * as reduxPersist from 'redux-persist'; +import { logger } from '@shared/logger'; import { getLogsFromBrowserStorage } from '@shared/logger-storage'; import { persistConfig } from '@shared/storage/redux-pesist'; @@ -29,7 +28,7 @@ const debug = { return reduxPersist.getStoredState(persistConfig); }, setHighestAccountIndex(index: number) { - toast.success('Highest account index set to ' + index); + logger.info(`Highest account index set to ${index}`); store.dispatch(stxChainSlice.actions.restoreAccountIndex(index)); }, resetMessages() { diff --git a/src/app/features/container/container.tsx b/src/app/features/container/container.tsx index 55d4ea9e5c8..173960eb015 100644 --- a/src/app/features/container/container.tsx +++ b/src/app/features/container/container.tsx @@ -1,5 +1,4 @@ import { useEffect } from 'react'; -import { Toaster } from 'react-hot-toast'; import { Outlet, useLocation } from 'react-router-dom'; import { closeWindow } from '@shared/utils'; @@ -35,7 +34,6 @@ export function Container() { <> - diff --git a/src/app/features/html-head/head-provider.tsx b/src/app/features/html-head/head-provider.tsx index fb1f9680572..9586cdc18a4 100644 --- a/src/app/features/html-head/head-provider.tsx +++ b/src/app/features/html-head/head-provider.tsx @@ -1,13 +1,13 @@ -import { Link, HeadProvider as ReastHeadProvider, Title } from 'react-head'; +import { Link, HeadProvider as ReactHeadProvider, Title } from 'react-head'; import { useNewBrandApprover } from '@app/store/settings/settings.selectors'; export function HeadProvider() { const { hasApprovedNewBrand } = useNewBrandApprover(); return ( - + {hasApprovedNewBrand ? : } - + ); } diff --git a/src/app/features/increase-fee-drawer/components/increase-stx-fee-form.tsx b/src/app/features/increase-fee-drawer/components/increase-stx-fee-form.tsx index 3d010093475..71baa6d5f36 100644 --- a/src/app/features/increase-fee-drawer/components/increase-stx-fee-form.tsx +++ b/src/app/features/increase-fee-drawer/components/increase-stx-fee-form.tsx @@ -1,5 +1,4 @@ import { useCallback, useEffect } from 'react'; -import { toast } from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; import BigNumber from 'bignumber.js'; @@ -18,6 +17,7 @@ import { stxFeeValidator } from '@app/common/validation/forms/fee-validators'; import { LoadingSpinner } from '@app/components/loading-spinner'; import { StacksTransactionItem } from '@app/components/stacks-transaction-item/stacks-transaction-item'; import { useStacksBroadcastTransaction } from '@app/features/stacks-transaction-request/hooks/use-stacks-broadcast-transaction'; +import { useToast } from '@app/features/toasts/use-toast'; import { useCurrentStacksAccountBalances } from '@app/query/stacks/balance/stx-balance.hooks'; import { useSubmittedTransactionsActions } from '@app/store/submitted-transactions/submitted-transactions.hooks'; import { useRawDeserializedTxState, useRawTxIdState } from '@app/store/transactions/raw.hooks'; @@ -28,6 +28,7 @@ import { IncreaseFeeActions } from './increase-fee-actions'; import { IncreaseFeeField } from './increase-fee-field'; export function IncreaseStxFeeForm() { + const toast = useToast(); const refreshAccountData = useRefreshAllAccountData(); const tx = useSelectedTx(); const navigate = useNavigate(); @@ -43,9 +44,9 @@ export function IncreaseStxFeeForm() { useEffect(() => { if (tx?.tx_status !== 'pending' && rawTx) { setTxId(null); - toast('Your transaction went through! No need to speed it up.'); + toast.info('Your transaction went through! No need to speed it up.'); } - }, [rawTx, tx?.tx_status, setTxId]); + }, [rawTx, tx?.tx_status, setTxId, toast]); const onSubmit = useCallback( async (values: any) => { diff --git a/src/app/features/increase-fee-drawer/hooks/use-btc-increase-fee.ts b/src/app/features/increase-fee-drawer/hooks/use-btc-increase-fee.ts index 212ec4d01f1..48d2a3528c3 100644 --- a/src/app/features/increase-fee-drawer/hooks/use-btc-increase-fee.ts +++ b/src/app/features/increase-fee-drawer/hooks/use-btc-increase-fee.ts @@ -1,4 +1,3 @@ -import { toast } from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; import * as btc from '@scure/btc-signer'; @@ -21,6 +20,7 @@ import { } from '@app/common/transactions/bitcoin/utils'; import { MAX_FEE_RATE_MULTIPLIER } from '@app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee'; import { useBitcoinFeesList } from '@app/components/bitcoin-fees-list/use-bitcoin-fees-list'; +import { useToast } from '@app/features/toasts/use-toast'; import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; import { useBitcoinBroadcastTransaction } from '@app/query/bitcoin/transaction/use-bitcoin-broadcast-transaction'; import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain'; @@ -28,6 +28,7 @@ import { useSignBitcoinTx } from '@app/store/accounts/blockchain/bitcoin/bitcoin import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; export function useBtcIncreaseFee(btcTx: BitcoinTx) { + const toast = useToast(); const navigate = useNavigate(); const networkMode = useBitcoinScureLibNetworkConfig(); const analytics = useAnalytics(); diff --git a/src/app/features/ledger/flows/bitcoin-tx-signing/ledger-bitcoin-sign-tx-container.tsx b/src/app/features/ledger/flows/bitcoin-tx-signing/ledger-bitcoin-sign-tx-container.tsx index 2ce87ffb6c1..a48b8bb90d5 100644 --- a/src/app/features/ledger/flows/bitcoin-tx-signing/ledger-bitcoin-sign-tx-container.tsx +++ b/src/app/features/ledger/flows/bitcoin-tx-signing/ledger-bitcoin-sign-tx-container.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from 'react'; -import toast from 'react-hot-toast'; import { Route, useLocation } from 'react-router-dom'; import * as btc from '@scure/btc-signer'; @@ -27,6 +26,7 @@ import { getBitcoinAppVersion, isBitcoinAppOpen, } from '@app/features/ledger/utils/bitcoin-ledger-utils'; +import { useToast } from '@app/features/toasts/use-toast'; import { useSignLedgerBitcoinTx } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; @@ -38,6 +38,7 @@ export const ledgerBitcoinTxSigningRoutes = ledgerSignTxRoutes({ }); function LedgerSignBitcoinTxContainer() { + const toast = useToast(); const location = useLocation(); const ledgerNavigate = useLedgerNavigate(); const ledgerAnalytics = useLedgerAnalytics(); diff --git a/src/app/features/ledger/flows/request-stacks-keys/ledger-request-stacks-keys.tsx b/src/app/features/ledger/flows/request-stacks-keys/ledger-request-stacks-keys.tsx index 930d0ba7c3e..b746bab2eee 100644 --- a/src/app/features/ledger/flows/request-stacks-keys/ledger-request-stacks-keys.tsx +++ b/src/app/features/ledger/flows/request-stacks-keys/ledger-request-stacks-keys.tsx @@ -1,4 +1,3 @@ -import toast from 'react-hot-toast'; import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; @@ -18,9 +17,11 @@ import { isStacksAppOpen, useActionCancellableByUser, } from '@app/features/ledger/utils/stacks-ledger-utils'; +import { useToast } from '@app/features/toasts/use-toast'; import { stacksKeysSlice } from '@app/store/ledger/stacks/stacks-key.slice'; function LedgerRequestStacksKeys() { + const toast = useToast(); const navigate = useNavigate(); const ledgerNavigate = useLedgerNavigate(); const canUserCancelAction = useActionCancellableByUser(); diff --git a/src/app/features/retrieve-taproot-to-native-segwit/retrieve-taproot-to-native-segwit.tsx b/src/app/features/retrieve-taproot-to-native-segwit/retrieve-taproot-to-native-segwit.tsx index 5a2b226b6db..51b0f5469cb 100644 --- a/src/app/features/retrieve-taproot-to-native-segwit/retrieve-taproot-to-native-segwit.tsx +++ b/src/app/features/retrieve-taproot-to-native-segwit/retrieve-taproot-to-native-segwit.tsx @@ -1,4 +1,3 @@ -import toast from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; import { Stack } from 'leather-styles/jsx'; @@ -10,6 +9,7 @@ import { formatMoneyPadded } from '@app/common/money/format-money'; import { delay } from '@app/common/utils'; import { FormAddressDisplayer } from '@app/components/address-displayer/form-address-displayer'; import { InfoCard, InfoCardRow, InfoCardSeparator } from '@app/components/info-card/info-card'; +import { useToast } from '@app/features/toasts/use-toast'; import { useCurrentTaprootAccountBalance, useCurrentTaprootAccountUninscribedUtxos, @@ -23,6 +23,7 @@ import { RetrieveTaprootToNativeSegwitLayout } from './components/retrieve-tapro import { useGenerateRetrieveTaprootFundsTx } from './use-generate-retrieve-taproot-funds-tx'; export function RetrieveTaprootToNativeSegwit() { + const toast = useToast(); const navigate = useNavigate(); const balance = useCurrentTaprootAccountBalance(); const recipient = useCurrentAccountNativeSegwitAddressIndexZero(); diff --git a/src/app/features/settings-dropdown/components/advanced-menu-items.tsx b/src/app/features/settings-dropdown/components/advanced-menu-items.tsx index 42f812a3c41..1dfcf684f02 100644 --- a/src/app/features/settings-dropdown/components/advanced-menu-items.tsx +++ b/src/app/features/settings-dropdown/components/advanced-menu-items.tsx @@ -1,6 +1,5 @@ import { useMemo } from 'react'; import { useAsync } from 'react-async-hook'; -import toast from 'react-hot-toast'; import { clearBrowserStorageLogs, @@ -10,6 +9,7 @@ import { import { isNumber } from '@shared/utils'; import { Divider } from '@app/components/layout/divider'; +import { useToast } from '@app/features/toasts/use-toast'; import { Caption } from '@app/ui/components/typography/caption'; import { SettingsMenuItem as MenuItem } from './settings-menu-item'; @@ -27,6 +27,7 @@ interface AdvancedMenuItemsProps { } export function AdvancedMenuItems({ closeHandler, settingsShown }: AdvancedMenuItemsProps) { const { result: logSizeInBytes } = useAsync(async () => getLogSizeInBytes(), [settingsShown]); + const toast = useToast(); const diagnosticLogText = useMemo(() => { const noLogInfoMsg = `There are no logs cached`; diff --git a/src/app/features/stacks-transaction-request/hooks/use-stacks-broadcast-transaction.tsx b/src/app/features/stacks-transaction-request/hooks/use-stacks-broadcast-transaction.tsx index 0faef75e0f4..5c8b8669990 100644 --- a/src/app/features/stacks-transaction-request/hooks/use-stacks-broadcast-transaction.tsx +++ b/src/app/features/stacks-transaction-request/hooks/use-stacks-broadcast-transaction.tsx @@ -1,5 +1,4 @@ import { useMemo, useState } from 'react'; -import toast from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; import { AuthType, StacksTransaction } from '@stacks/transactions'; @@ -15,6 +14,7 @@ import { LoadingKeys } from '@app/common/hooks/use-loading'; import { useSubmitTransactionCallback } from '@app/common/hooks/use-submit-stx-transaction'; import { stacksTransactionToHex } from '@app/common/transactions/stacks/transaction.utils'; import { delay } from '@app/common/utils'; +import { useToast } from '@app/features/toasts/use-toast'; import { useTransactionRequest } from '@app/store/transactions/requests.hooks'; import { useSignStacksTransaction } from '@app/store/transactions/transaction.hooks'; @@ -31,6 +31,7 @@ export function useStacksBroadcastTransaction(token: CryptoCurrencies, decimals? const requestToken = useTransactionRequest(); const { formSentSummaryTxState } = useStacksTransactionSummary(token); const navigate = useNavigate(); + const toast = useToast(); const broadcastTransactionFn = useSubmitTransactionCallback({ loadingKey: LoadingKeys.SUBMIT_SEND_FORM_TRANSACTION, @@ -107,14 +108,15 @@ export function useStacksBroadcastTransaction(token: CryptoCurrencies, decimals? isBroadcasting, }; }, [ - broadcastTransactionFn, - navigate, - signStacksTransaction, isBroadcasting, + requestToken, + tabId, + navigate, token, formSentSummaryTxState, decimals, - requestToken, - tabId, + toast, + broadcastTransactionFn, + signStacksTransaction, ]); } diff --git a/src/app/features/toasts/toasts.tsx b/src/app/features/toasts/toasts.tsx new file mode 100644 index 00000000000..b2edc5b7d9c --- /dev/null +++ b/src/app/features/toasts/toasts.tsx @@ -0,0 +1,58 @@ +import { useMemo } from 'react'; + +import type { HasChildren } from '@app/common/has-children'; +import { Toast } from '@app/ui/components/toast/toast'; +import { ToastLayout } from '@app/ui/components/toast/toast.layout'; + +import { ToastContext } from './use-toast'; +import { useToastHandlers } from './use-toast-handlers'; + +function Toasts(props: HasChildren) { + const { children } = props; + + const { + toasts, + handleRemoveToast, + handleDispatchError, + handleDispatchInfo, + handleDispatchSuccess, + handleDispatchPromise, + } = useToastHandlers(); + + return ( + ({ + error: handleDispatchError, + info: handleDispatchInfo, + success: handleDispatchSuccess, + promise: handleDispatchPromise, + }), + [handleDispatchError, handleDispatchInfo, handleDispatchSuccess, handleDispatchPromise] + )} + > + + {children} + {Array.from(toasts).map(([key, toast]) => { + return ( + { + if (!open) handleRemoveToast(key); + }} + > + + + + + ); + })} + + + + ); +} + +export { Toasts }; diff --git a/src/app/features/toasts/use-toast-handlers.ts b/src/app/features/toasts/use-toast-handlers.ts new file mode 100644 index 00000000000..b8adbedf155 --- /dev/null +++ b/src/app/features/toasts/use-toast-handlers.ts @@ -0,0 +1,51 @@ +import { useCallback, useState } from 'react'; + +import type { ToastProps } from '@app/ui/components/toast/toast'; + +export function useToastHandlers() { + const [toasts, setToasts] = useState(new Map()); + + const handleAddToast = useCallback((toast: ToastProps) => { + setToasts(prev => { + const newMap = new Map(prev); + newMap.set(String(Date.now()), { ...toast }); + return newMap; + }); + }, []); + + return { + toasts, + handleRemoveToast: useCallback((key: any) => { + setToasts(prev => { + const newMap = new Map(prev); + newMap.delete(key); + return newMap; + }); + }, []), + handleDispatchInfo: useCallback( + (message: string) => handleAddToast({ message, variant: 'info' }), + [handleAddToast] + ), + handleDispatchSuccess: useCallback( + (message: string) => handleAddToast({ message, variant: 'success' }), + [handleAddToast] + ), + handleDispatchError: useCallback( + (message: string) => handleAddToast({ message, variant: 'error' }), + [handleAddToast] + ), + handleDispatchPromise: useCallback( + async (promise: Promise, msgs: { success: string; error: string }) => { + return promise + .then(data => { + handleAddToast({ message: msgs.success, variant: 'success' }); + return data; + }) + .catch(() => { + handleAddToast({ message: msgs.error, variant: 'error' }); + }); + }, + [handleAddToast] + ), + }; +} diff --git a/src/app/features/toasts/use-toast.ts b/src/app/features/toasts/use-toast.ts new file mode 100644 index 00000000000..293a487f09b --- /dev/null +++ b/src/app/features/toasts/use-toast.ts @@ -0,0 +1,15 @@ +import { createContext, useContext } from 'react'; + +interface ToastContextProps { + error(message: string): void; + info(message: string): void; + success(message: string): void; + promise(promise: Promise, msgs: { success: string; error: string }): Promise; +} +export const ToastContext = createContext(null); + +export function useToast() { + const context = useContext(ToastContext); + if (context) return context; + throw new Error('useToast must be used within Toasts'); +} diff --git a/src/app/pages/receive/receive-btc.tsx b/src/app/pages/receive/receive-btc.tsx index a91b73be5e7..8ac2e749423 100644 --- a/src/app/pages/receive/receive-btc.tsx +++ b/src/app/pages/receive/receive-btc.tsx @@ -1,10 +1,10 @@ -import toast from 'react-hot-toast'; import { useLocation } from 'react-router-dom'; import get from 'lodash.get'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { useClipboard } from '@app/common/hooks/use-copy-to-clipboard'; +import { useToast } from '@app/features/toasts/use-toast'; import { useBackgroundLocationRedirect } from '@app/routes/hooks/use-background-location-redirect'; import { useCurrentAccountIndex } from '@app/store/accounts/account'; import { useNativeSegwitAccountIndexAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; @@ -19,6 +19,7 @@ export function ReceiveBtcModal({ type = 'btc' }: ReceiveBtcModalType) { useBackgroundLocationRedirect(); const analytics = useAnalytics(); const { state } = useLocation(); + const toast = useToast(); const currentAccountIndex = useCurrentAccountIndex(); const accountIndex = get(state, 'accountIndex', currentAccountIndex); diff --git a/src/app/pages/receive/receive-modal.tsx b/src/app/pages/receive/receive-modal.tsx index b39ca8b0e75..e705dc3f779 100644 --- a/src/app/pages/receive/receive-modal.tsx +++ b/src/app/pages/receive/receive-modal.tsx @@ -1,4 +1,3 @@ -import toast from 'react-hot-toast'; import { useLocation, useNavigate } from 'react-router-dom'; import { HomePageSelectors } from '@tests/selectors/home.selectors'; @@ -11,6 +10,7 @@ import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { useClipboard } from '@app/common/hooks/use-copy-to-clipboard'; import { useLocationState } from '@app/common/hooks/use-location-state'; import { BaseDrawer } from '@app/components/drawer/base-drawer'; +import { useToast } from '@app/features/toasts/use-toast'; import { useBackgroundLocationRedirect } from '@app/routes/hooks/use-background-location-redirect'; import { useZeroIndexTaprootAddress } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks'; import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; @@ -31,6 +31,7 @@ interface ReceiveModalProps { export function ReceiveModal({ type = 'full' }: ReceiveModalProps) { useBackgroundLocationRedirect(); + const toast = useToast(); const analytics = useAnalytics(); const backgroundLocation = useLocationState('backgroundLocation'); const navigate = useNavigate(); diff --git a/src/app/pages/receive/receive-ordinal.tsx b/src/app/pages/receive/receive-ordinal.tsx index a061131caca..ed6e4118650 100644 --- a/src/app/pages/receive/receive-ordinal.tsx +++ b/src/app/pages/receive/receive-ordinal.tsx @@ -1,8 +1,8 @@ -import toast from 'react-hot-toast'; import { useLocation } from 'react-router-dom'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { useClipboard } from '@app/common/hooks/use-copy-to-clipboard'; +import { useToast } from '@app/features/toasts/use-toast'; import { useBackgroundLocationRedirect } from '@app/routes/hooks/use-background-location-redirect'; import { ReceiveBtcModalWarning } from './components/receive-btc-warning'; @@ -10,6 +10,7 @@ import { ReceiveTokensLayout } from './components/receive-tokens.layout'; export function ReceiveOrdinalModal() { useBackgroundLocationRedirect(); + const toast = useToast(); const analytics = useAnalytics(); const { state } = useLocation(); const { onCopy } = useClipboard(state.btcAddressTaproot); diff --git a/src/app/pages/receive/receive-stx.tsx b/src/app/pages/receive/receive-stx.tsx index c785a0e7de1..4d4708ade08 100644 --- a/src/app/pages/receive/receive-stx.tsx +++ b/src/app/pages/receive/receive-stx.tsx @@ -1,8 +1,7 @@ -import toast from 'react-hot-toast'; - import { useCurrentAccountDisplayName } from '@app/common/hooks/account/use-account-names'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { useClipboard } from '@app/common/hooks/use-copy-to-clipboard'; +import { useToast } from '@app/features/toasts/use-toast'; import { useBackgroundLocationRedirect } from '@app/routes/hooks/use-background-location-redirect'; import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; @@ -10,6 +9,7 @@ import { ReceiveTokensLayout } from './components/receive-tokens.layout'; export function ReceiveStxModal() { useBackgroundLocationRedirect(); + const toast = useToast(); const currentAccount = useCurrentStacksAccount(); const analytics = useAnalytics(); const { onCopy } = useClipboard(currentAccount?.address ?? ''); diff --git a/src/app/pages/rpc-send-transfer/rpc-send-transfer-summary.tsx b/src/app/pages/rpc-send-transfer/rpc-send-transfer-summary.tsx index 73d334658a0..c2d2474e5c9 100644 --- a/src/app/pages/rpc-send-transfer/rpc-send-transfer-summary.tsx +++ b/src/app/pages/rpc-send-transfer/rpc-send-transfer-summary.tsx @@ -1,4 +1,3 @@ -import toast from 'react-hot-toast'; import { useLocation } from 'react-router-dom'; import { HStack, Stack } from 'leather-styles/jsx'; @@ -15,6 +14,7 @@ import { InfoCardRow, InfoCardSeparator, } from '@app/components/info-card/info-card'; +import { useToast } from '@app/features/toasts/use-toast'; import { CheckmarkIcon } from '@app/ui/icons/checkmark-icon'; import { CopyIcon } from '@app/ui/icons/copy-icon'; import { ExternalLinkIcon } from '@app/ui/icons/external-link-icon'; @@ -23,6 +23,7 @@ export function RpcSendTransferSummary() { const { state } = useLocation(); const { handleOpenBitcoinTxLink: handleOpenTxLink } = useBitcoinExplorerLink(); const analytics = useAnalytics(); + const toast = useToast(); const { txId, diff --git a/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts b/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts index c35a0dd2696..7db34e2510a 100644 --- a/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts +++ b/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts @@ -1,5 +1,4 @@ import { useMemo, useState } from 'react'; -import toast from 'react-hot-toast'; import { PaymentTypes, RpcErrorCode } from '@btckit/types'; import * as btc from '@scure/btc-signer'; @@ -14,6 +13,7 @@ import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params'; import { initialSearchParams } from '@app/common/initial-search-params'; import { createDelay } from '@app/common/utils'; +import { useToast } from '@app/features/toasts/use-toast'; import { useSignBitcoinTx } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks'; import { useCurrentAccountNativeSegwitSigner, @@ -52,6 +52,7 @@ function useSignBip322MessageFactory({ address, signPsbt }: SignBip322MessageFac const network = useCurrentNetwork(); const analytics = useAnalytics(); const [isLoading, setIsLoading] = useState(false); + const toast = useToast(); const { tabId, origin, requestId, message } = useRpcSignBitcoinMessage(); diff --git a/src/app/pages/rpc-sign-psbt/rpc-sign-psbt-summary.tsx b/src/app/pages/rpc-sign-psbt/rpc-sign-psbt-summary.tsx index 24795f34fd1..2bcb2d2cb40 100644 --- a/src/app/pages/rpc-sign-psbt/rpc-sign-psbt-summary.tsx +++ b/src/app/pages/rpc-sign-psbt/rpc-sign-psbt-summary.tsx @@ -1,4 +1,3 @@ -import toast from 'react-hot-toast'; import { useLocation } from 'react-router-dom'; import { Flex, HStack, Stack } from 'leather-styles/jsx'; @@ -13,6 +12,7 @@ import { InfoCardFooter, InfoCardRow, } from '@app/components/info-card/info-card'; +import { useToast } from '@app/features/toasts/use-toast'; import { CheckmarkIcon } from '@app/ui/icons/checkmark-icon'; import { CopyIcon } from '@app/ui/icons/copy-icon'; import { ExternalLinkIcon } from '@app/ui/icons/external-link-icon'; @@ -21,6 +21,7 @@ export function RpcSignPsbtSummary() { const { state } = useLocation(); const { handleOpenBitcoinTxLink: handleOpenTxLink } = useBitcoinExplorerLink(); const analytics = useAnalytics(); + const toast = useToast(); const { fee, sendingValue, totalSpend, txId, txFiatValue, txFiatValueSymbol, txLink, txValue } = state; diff --git a/src/app/pages/send/choose-crypto-asset/choose-crypto-asset.tsx b/src/app/pages/send/choose-crypto-asset/choose-crypto-asset.tsx index 5e8c25d976b..f23e1e15192 100644 --- a/src/app/pages/send/choose-crypto-asset/choose-crypto-asset.tsx +++ b/src/app/pages/send/choose-crypto-asset/choose-crypto-asset.tsx @@ -1,4 +1,3 @@ -import toast from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; import { AllTransferableCryptoAssetBalances } from '@shared/models/crypto-asset-balance.model'; @@ -10,10 +9,12 @@ import { useWalletType } from '@app/common/use-wallet-type'; import { ChooseCryptoAssetLayout } from '@app/components/crypto-assets/choose-crypto-asset/choose-crypto-asset.layout'; import { CryptoAssetList } from '@app/components/crypto-assets/choose-crypto-asset/crypto-asset-list'; import { ModalHeader } from '@app/components/modal-header'; +import { useToast } from '@app/features/toasts/use-toast'; import { useConfigBitcoinSendEnabled } from '@app/query/common/remote-config/remote-config.query'; import { useCheckLedgerBlockchainAvailable } from '@app/store/accounts/blockchain/utils'; export function ChooseCryptoAsset() { + const toast = useToast(); const allTransferableCryptoAssetBalances = useAllTransferableCryptoAssetBalances(); const { whenWallet } = useWalletType(); diff --git a/src/app/pages/send/locked-bitcoin-summary/locked-bitcoin-summary.tsx b/src/app/pages/send/locked-bitcoin-summary/locked-bitcoin-summary.tsx index c8a5f034a2d..a5650b7f630 100644 --- a/src/app/pages/send/locked-bitcoin-summary/locked-bitcoin-summary.tsx +++ b/src/app/pages/send/locked-bitcoin-summary/locked-bitcoin-summary.tsx @@ -1,4 +1,3 @@ -import { toast } from 'react-hot-toast'; import { useLocation } from 'react-router-dom'; import { HStack, styled } from 'leather-styles/jsx'; @@ -15,12 +14,14 @@ import { InfoCardFooter, } from '@app/components/info-card/info-card'; import { ModalHeader } from '@app/components/modal-header'; +import { useToast } from '@app/features/toasts/use-toast'; import { CheckmarkIcon } from '@app/ui/icons/checkmark-icon'; import { CopyIcon } from '@app/ui/icons/copy-icon'; import { ExternalLinkIcon } from '@app/ui/icons/external-link-icon'; export function LockBitcoinSummary() { const { state } = useLocation(); + const toast = useToast(); const { txId, txMoney, txFiatValue, txFiatValueSymbol, symbol, txLink } = state; diff --git a/src/app/pages/send/ordinal-inscription/sent-inscription-summary.tsx b/src/app/pages/send/ordinal-inscription/sent-inscription-summary.tsx index 7a1b051e65b..89d616d09ef 100644 --- a/src/app/pages/send/ordinal-inscription/sent-inscription-summary.tsx +++ b/src/app/pages/send/ordinal-inscription/sent-inscription-summary.tsx @@ -1,4 +1,3 @@ -import { toast } from 'react-hot-toast'; import { useLocation, useNavigate } from 'react-router-dom'; import { Box, HStack, Stack } from 'leather-styles/jsx'; @@ -20,6 +19,7 @@ import { InfoCardSeparator, } from '@app/components/info-card/info-card'; import { InscriptionPreview } from '@app/components/inscription-preview-card/components/inscription-preview'; +import { useToast } from '@app/features/toasts/use-toast'; import { CheckmarkIcon } from '@app/ui/icons/checkmark-icon'; import { CopyIcon } from '@app/ui/icons/copy-icon'; import { ExternalLinkIcon } from '@app/ui/icons/external-link-icon'; @@ -39,7 +39,7 @@ function useSendInscriptionSummaryState() { export function SendInscriptionSummary() { const { txid, recipient, arrivesIn, inscription, feeRowValue } = useSendInscriptionSummaryState(); - + const toast = useToast(); const navigate = useNavigate(); const txLink = { blockchain: 'bitcoin' as Blockchains, diff --git a/src/app/pages/send/send-crypto-asset-form/components/send-max-button.tsx b/src/app/pages/send/send-crypto-asset-form/components/send-max-button.tsx index 22f99110ec0..344d9f8e0df 100644 --- a/src/app/pages/send/send-crypto-asset-form/components/send-max-button.tsx +++ b/src/app/pages/send/send-crypto-asset-form/components/send-max-button.tsx @@ -1,5 +1,4 @@ import { useCallback } from 'react'; -import toast from 'react-hot-toast'; import { SendCryptoAssetSelectors } from '@tests/selectors/send.selectors'; import { useField } from 'formik'; @@ -8,6 +7,7 @@ import { Box } from 'leather-styles/jsx'; import { Money } from '@shared/models/money.model'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; +import { useToast } from '@app/features/toasts/use-toast'; import { Link } from '@app/ui/components/link/link'; interface SendMaxButtonProps { @@ -16,14 +16,18 @@ interface SendMaxButtonProps { } export function SendMaxButton({ balance, sendMaxBalance, ...props }: SendMaxButtonProps) { const [, _, amountFieldHelpers] = useField('amount'); + const toast = useToast(); const analytics = useAnalytics(); const onSendMax = useCallback(() => { void analytics.track('select_maximum_amount_for_send'); - if (balance.amount.isLessThanOrEqualTo(0)) return toast.error(`Zero balance`); + if (balance.amount.isLessThanOrEqualTo(0)) { + toast.error('Zero balance'); + return; + } return amountFieldHelpers.setValue(sendMaxBalance); - }, [amountFieldHelpers, analytics, balance.amount, sendMaxBalance]); + }, [amountFieldHelpers, analytics, balance.amount, sendMaxBalance, toast]); // Hide send max button if lowest fee calc is greater // than available balance which will default to zero diff --git a/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-send-max.tsx b/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-send-max.tsx index 9f91ee81981..1f9de4221b2 100644 --- a/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-send-max.tsx +++ b/src/app/pages/send/send-crypto-asset-form/family/bitcoin/hooks/use-send-max.tsx @@ -1,11 +1,11 @@ import { useCallback } from 'react'; -import toast from 'react-hot-toast'; import { useField } from 'formik'; import { Money } from '@shared/models/money.model'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; +import { useToast } from '@app/features/toasts/use-toast'; interface UseSendMaxArgs { balance: Money; @@ -23,19 +23,20 @@ export function useSendMax({ }: UseSendMaxArgs) { const [, _, amountFieldHelpers] = useField('amount'); const [, __, feeFieldHelpers] = useField('fee'); + const toast = useToast(); const analytics = useAnalytics(); return useCallback(() => { void analytics.track('select_maximum_amount_for_send'); if (balance.amount.isLessThanOrEqualTo(0)) { - toast.error(`Zero balance`); + toast.error('Zero balance'); return; } onSetIsSendingMax(!isSendingMax); - feeFieldHelpers.setValue(sendMaxFee); - amountFieldHelpers.setValue(sendMaxBalance, false); - amountFieldHelpers.setTouched(false, false); + void feeFieldHelpers.setValue(sendMaxFee); + void amountFieldHelpers.setValue(sendMaxBalance, false); + void amountFieldHelpers.setTouched(false, false); amountFieldHelpers.setError(undefined); }, [ amountFieldHelpers, @@ -46,5 +47,6 @@ export function useSendMax({ onSetIsSendingMax, sendMaxBalance, sendMaxFee, + toast, ]); } diff --git a/src/app/pages/send/send-crypto-asset-form/form/brc-20/brc-20-choose-fee.tsx b/src/app/pages/send/send-crypto-asset-form/form/brc-20/brc-20-choose-fee.tsx index 7ee86e62a8f..8c8cc960982 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/brc-20/brc-20-choose-fee.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/brc-20/brc-20-choose-fee.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { toast } from 'react-hot-toast'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { Stack } from 'leather-styles/jsx'; @@ -22,6 +21,7 @@ import { LoadingSpinner } from '@app/components/loading-spinner'; import { ModalHeader } from '@app/components/modal-header'; import { BitcoinChooseFee } from '@app/features/bitcoin-choose-fee/bitcoin-choose-fee'; import { useValidateBitcoinSpend } from '@app/features/bitcoin-choose-fee/hooks/use-validate-bitcoin-spend'; +import { useToast } from '@app/features/toasts/use-toast'; import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client'; import { useBrc20Transfers } from '@app/query/bitcoin/ordinals/brc20/use-brc-20'; import { useSignBitcoinTx } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks'; @@ -40,6 +40,7 @@ function useBrc20ChooseFeeState() { } export function BrcChooseFee() { + const toast = useToast(); const navigate = useNavigate(); const { amount, recipient, ticker, utxos, holderAddress } = useBrc20ChooseFeeState(); const generateTx = useGenerateUnsignedNativeSegwitSingleRecipientTx(); diff --git a/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/sip10-token-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/sip10-token-send-form.tsx index a51f86323a4..ea019caa5cd 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/sip10-token-send-form.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/sip10-token-send-form.tsx @@ -1,8 +1,9 @@ -import { toast } from 'react-hot-toast'; import { Navigate, useParams } from 'react-router-dom'; import { RouteUrls } from '@shared/route-urls'; +import { useToast } from '@app/features/toasts/use-toast'; + import { Sip10TokenSendFormContainer } from './sip10-token-send-form-container'; export function Sip10TokenSendForm() { @@ -20,6 +21,8 @@ interface Sip10TokenSendFormLoaderProps { } function Sip10TokenSendFormLoader({ children }: Sip10TokenSendFormLoaderProps) { const { symbol, contractId } = useParams(); + const toast = useToast(); + if (!symbol || !contractId) { toast.error('Symbol or contract id not found'); return ; diff --git a/src/app/pages/send/sent-summary/btc-sent-summary.tsx b/src/app/pages/send/sent-summary/btc-sent-summary.tsx index 2a641c41ee9..8dd0f56b216 100644 --- a/src/app/pages/send/sent-summary/btc-sent-summary.tsx +++ b/src/app/pages/send/sent-summary/btc-sent-summary.tsx @@ -1,4 +1,3 @@ -import { toast } from 'react-hot-toast'; import { useLocation } from 'react-router-dom'; import { HStack, Stack } from 'leather-styles/jsx'; @@ -17,6 +16,7 @@ import { InfoCardSeparator, } from '@app/components/info-card/info-card'; import { ModalHeader } from '@app/components/modal-header'; +import { useToast } from '@app/features/toasts/use-toast'; import { CopyIcon } from '@app/ui/icons/copy-icon'; import { ExternalLinkIcon } from '@app/ui/icons/external-link-icon'; @@ -24,6 +24,8 @@ import { TxDone } from '../send-crypto-asset-form/components/tx-done'; export function BtcSentSummary() { const { state } = useLocation(); + const analytics = useAnalytics(); + const toast = useToast(); const { txId, @@ -41,7 +43,6 @@ export function BtcSentSummary() { const { onCopy } = useClipboard(txId); const { handleOpenBitcoinTxLink: handleOpenTxLink } = useBitcoinExplorerLink(); - const analytics = useAnalytics(); function onClickLink() { void analytics.track('view_transaction_confirmation', { symbol: 'BTC' }); diff --git a/src/app/pages/send/sent-summary/stx-sent-summary.tsx b/src/app/pages/send/sent-summary/stx-sent-summary.tsx index a8f1d6e4fda..8346b8c9cce 100644 --- a/src/app/pages/send/sent-summary/stx-sent-summary.tsx +++ b/src/app/pages/send/sent-summary/stx-sent-summary.tsx @@ -1,4 +1,3 @@ -import { toast } from 'react-hot-toast'; import { useLocation } from 'react-router-dom'; import { HStack, Stack } from 'leather-styles/jsx'; @@ -18,6 +17,7 @@ import { InfoCardSeparator, } from '@app/components/info-card/info-card'; import { ModalHeader } from '@app/components/modal-header'; +import { useToast } from '@app/features/toasts/use-toast'; import { CopyIcon } from '@app/ui/icons/copy-icon'; import { ExternalLinkIcon } from '@app/ui/icons/external-link-icon'; @@ -25,6 +25,8 @@ import { TxDone } from '../send-crypto-asset-form/components/tx-done'; export function StxSentSummary() { const { state } = useLocation(); + const analytics = useAnalytics(); + const toast = useToast(); const { txValue, @@ -42,7 +44,6 @@ export function StxSentSummary() { const { onCopy } = useClipboard(txId || ''); const { handleOpenStacksTxLink: handleOpenTxLink } = useStacksExplorerLink(); - const analytics = useAnalytics(); function onClickLink() { void analytics.track('view_transaction_confirmation', { symbol: 'STX' }); diff --git a/src/app/pages/swap/hooks/use-stacks-broadcast-swap.tsx b/src/app/pages/swap/hooks/use-stacks-broadcast-swap.tsx index 4046cd2ec20..0a4cd1ec6e4 100644 --- a/src/app/pages/swap/hooks/use-stacks-broadcast-swap.tsx +++ b/src/app/pages/swap/hooks/use-stacks-broadcast-swap.tsx @@ -1,5 +1,4 @@ import { useCallback } from 'react'; -import toast from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; import { StacksTransaction } from '@stacks/transactions'; @@ -10,10 +9,12 @@ import { isError, isString } from '@shared/utils'; import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading'; import { useSubmitTransactionCallback } from '@app/common/hooks/use-submit-stx-transaction'; +import { useToast } from '@app/features/toasts/use-toast'; export function useStacksBroadcastSwap() { const { setIsIdle } = useLoading(LoadingKeys.SUBMIT_SWAP_TRANSACTION); const navigate = useNavigate(); + const toast = useToast(); const broadcastTransactionFn = useSubmitTransactionCallback({ loadingKey: LoadingKeys.SUBMIT_SWAP_TRANSACTION, @@ -48,6 +49,6 @@ export function useStacksBroadcastSwap() { setIsIdle(); } }, - [broadcastTransactionFn, setIsIdle, navigate] + [toast, broadcastTransactionFn, setIsIdle, navigate] ); } diff --git a/src/app/query/stacks/balance/stacks-ft-balances.hooks.ts b/src/app/query/stacks/balance/stacks-ft-balances.hooks.ts index 7458e315ee5..f7059f30df3 100644 --- a/src/app/query/stacks/balance/stacks-ft-balances.hooks.ts +++ b/src/app/query/stacks/balance/stacks-ft-balances.hooks.ts @@ -1,5 +1,4 @@ import { useMemo } from 'react'; -import { toast } from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; import BigNumber from 'bignumber.js'; @@ -7,6 +6,7 @@ import BigNumber from 'bignumber.js'; import type { StacksFungibleTokenAssetBalance } from '@shared/models/crypto-asset-balance.model'; import { formatContractId } from '@app/common/utils'; +import { useToast } from '@app/features/toasts/use-toast'; import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useGetFungibleTokenMetadataListQuery } from '../tokens/fungible-tokens/fungible-token-metadata.query'; @@ -58,9 +58,11 @@ export function useStacksFungibleTokenAssetBalancesWithMetadata(address: string) } export function useStacksFungibleTokenAssetBalance(contractId: string) { + const toast = useToast(); const account = useCurrentStacksAccount(); const navigate = useNavigate(); const assetBalances = useStacksFungibleTokenAssetBalancesWithMetadata(account?.address ?? ''); + return useMemo(() => { const balance = assetBalances.find(assetBalance => assetBalance.asset.contractId.includes(contractId) @@ -70,7 +72,7 @@ export function useStacksFungibleTokenAssetBalance(contractId: string) { navigate('..'); } return balance ?? createStacksFtCryptoAssetBalanceTypeWrapper(new BigNumber(0), contractId); - }, [assetBalances, contractId, navigate]); + }, [assetBalances, contractId, navigate, toast]); } export function useTransferableStacksFungibleTokenAssetBalances( diff --git a/src/app/store/transactions/transaction.hooks.ts b/src/app/store/transactions/transaction.hooks.ts index d760d6488af..245ef93f4e1 100644 --- a/src/app/store/transactions/transaction.hooks.ts +++ b/src/app/store/transactions/transaction.hooks.ts @@ -1,6 +1,5 @@ import { useCallback, useMemo } from 'react'; import { useAsync } from 'react-async-hook'; -import toast from 'react-hot-toast'; import { bytesToHex } from '@noble/hashes/utils'; import { TransactionTypes } from '@stacks/connect'; @@ -28,6 +27,7 @@ import { import { useWalletType } from '@app/common/use-wallet-type'; import { listenForStacksTxLedgerSigning } from '@app/features/ledger/flows/stacks-tx-signing/stacks-tx-signing-event-listeners'; import { useLedgerNavigate } from '@app/features/ledger/hooks/use-ledger-navigate'; +import { useToast } from '@app/features/toasts/use-toast'; import { useNextNonce } from '@app/query/stacks/nonce/account-nonces.hooks'; import { useCurrentAccountStxAddressState, @@ -131,7 +131,9 @@ function useUnsignedStacksTransaction(values: StacksTransactionFormValues) { } function useSignTransactionSoftwareWallet() { + const toast = useToast(); const account = useCurrentStacksAccount(); + return useCallback( (tx: StacksTransaction) => { if (account?.type !== 'software') { @@ -145,7 +147,7 @@ function useSignTransactionSoftwareWallet() { signer.signOrigin(createStacksPrivateKey(account.stxPrivateKey)); return tx; }, - [account] + [account, toast.error] ); } diff --git a/src/app/ui/components/toast/toast.layout.tsx b/src/app/ui/components/toast/toast.layout.tsx new file mode 100644 index 00000000000..80a2fc0574f --- /dev/null +++ b/src/app/ui/components/toast/toast.layout.tsx @@ -0,0 +1,15 @@ +import { Box, HStack, styled } from 'leather-styles/jsx'; + +import { type ToastProps } from './toast'; +import { getIconVariant } from './toast.utils'; + +export function ToastLayout({ message, variant }: ToastProps) { + return ( + + {getIconVariant(variant)} + + {message} + + + ); +} diff --git a/src/app/ui/components/toast/toast.stories.tsx b/src/app/ui/components/toast/toast.stories.tsx new file mode 100644 index 00000000000..038ff3e8fd9 --- /dev/null +++ b/src/app/ui/components/toast/toast.stories.tsx @@ -0,0 +1,92 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { styled } from 'leather-styles/jsx'; + +import { Toast as Component } from './toast'; +import { ToastLayout } from './toast.layout'; + +const meta: Meta = { + component: Component.Root, + tags: ['autodocs'], + title: 'Toast', +}; + +export default meta; +type Story = StoryObj; + +export const Toast: Story = { + render: () => ( + + + + + + + + + + + ), +}; + +export const WithLongMessage: Story = { + render: () => ( + + + + + + + + + + + ), +}; + +export const AnimatedInfo: Story = { + render: () => ( + + + + + + + + + + + ), +}; + +export const AnimatedSuccess: Story = { + render: () => ( + + + + + + + + + + + ), +}; + +export const AnimatedError: Story = { + render: () => ( + + + + + + + + + + + ), +}; diff --git a/src/app/ui/components/toast/toast.tsx b/src/app/ui/components/toast/toast.tsx new file mode 100644 index 00000000000..b0f1cac5534 --- /dev/null +++ b/src/app/ui/components/toast/toast.tsx @@ -0,0 +1,60 @@ +import { forwardRef } from 'react'; + +import * as RadixToast from '@radix-ui/react-toast'; +import { css } from 'leather-styles/css'; + +export type ToastVariant = 'info' | 'success' | 'error'; + +export interface ToastProps { + message: string; + variant: ToastVariant; +} + +const toastRootStyles = css({ + bg: 'ink.text-primary', + color: 'ink.background-primary', + boxShadow: + '0px 12px 24px 0px rgba(18, 16, 15, 0.08), 0px 4px 8px 0px rgba(18, 16, 15, 0.08), 0px 0px 2px 0px rgba(18, 16, 15, 0.08)', + maxWidth: { base: '452px', smOnly: '342px' }, + pl: 'space.03', + pr: 'space.04', + py: 'space.03', + rounded: 'xs', + + '&[data-state=open]': { + animation: 'toastAppear 160ms cubic-bezier(0, 0.45, 0.6, 1) forwards', + }, + '&[data-state=closed]': { + animation: 'fadeout', + }, +}); +const Root: typeof RadixToast.Root = forwardRef((props, ref) => ( + +)); + +const toastViewportStyles = css({ + alignItems: 'center', + display: 'flex', + flexDirection: 'column', + gap: 'space.02', + height: 'fit-content', + m: '0 auto', + outline: 'none', + p: 'space.05', + position: 'fixed', + top: 0, + left: '50%', + transform: 'translate(-50%, 0)', + width: '100%', + zIndex: 9999, +}); +const Viewport: typeof RadixToast.Viewport = forwardRef((props, ref) => ( + +)); + +export const Toast = { + Provider: RadixToast.Provider, + Title: RadixToast.Title, + Root, + Viewport, +}; diff --git a/src/app/ui/components/toast/toast.utils.tsx b/src/app/ui/components/toast/toast.utils.tsx new file mode 100644 index 00000000000..6936725e968 --- /dev/null +++ b/src/app/ui/components/toast/toast.utils.tsx @@ -0,0 +1,16 @@ +import { CheckmarkCircleIcon, ErrorIcon, InfoCircleIcon } from '@app/ui/icons'; + +import type { ToastVariant } from './toast'; + +export function getIconVariant(variant?: ToastVariant) { + switch (variant) { + case 'info': + return ; + case 'success': + return ; + case 'error': + return ; + default: + return ; + } +} diff --git a/theme/keyframes.ts b/theme/keyframes.ts index cb3c6e181da..beee9e090e6 100644 --- a/theme/keyframes.ts +++ b/theme/keyframes.ts @@ -8,4 +8,8 @@ export const keyframes: CssKeyframes = { from: { opacity: 1, transform: 'translateY(0)' }, to: { opacity: 0, transform: 'translateY(4px)' }, }, + toastAppear: { + from: { opacity: 0, transform: 'translateY(-12px) scale(0.9)' }, + to: { opacity: 1, transform: 'translateY(0) scale(1)' }, + }, }; diff --git a/yarn.lock b/yarn.lock index 5028e95db80..a66818277a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3692,6 +3692,25 @@ "@radix-ui/react-roving-focus" "1.0.4" "@radix-ui/react-use-controllable-state" "1.0.1" +"@radix-ui/react-toast@1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.1.5.tgz#f5788761c0142a5ae9eb97f0051fd3c48106d9e6" + integrity sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-collection" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-visually-hidden" "1.0.3" + "@radix-ui/react-toggle-group@1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.0.4.tgz#f5b5c8c477831b013bec3580c55e20a68179d6ec"