diff --git a/dapp/src/assets/icons/AlertInfo.tsx b/dapp/src/assets/icons/AlertInfo.tsx deleted file mode 100644 index a1c8cf1ad..000000000 --- a/dapp/src/assets/icons/AlertInfo.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react" -import { createIcon } from "@chakra-ui/react" - -export const AlertInfo = createIcon({ - displayName: "AlertInfo", - viewBox: "0 0 16 16", - path: [ - - - , - - - - - , - ], -}) diff --git a/dapp/src/assets/icons/index.ts b/dapp/src/assets/icons/index.ts index 855a92790..d786957f5 100644 --- a/dapp/src/assets/icons/index.ts +++ b/dapp/src/assets/icons/index.ts @@ -3,7 +3,6 @@ export * from "./ArrowUpRight" export * from "./ArrowLeft" export * from "./ArrowRight" export * from "./AcreLogo" -export * from "./AlertInfo" export * from "./stBTC" export * from "./BTC" export * from "./ShieldPlus" diff --git a/dapp/src/components/Header/ConnectWallet.tsx b/dapp/src/components/Header/ConnectWallet.tsx index 9aee57748..0398874df 100644 --- a/dapp/src/components/Header/ConnectWallet.tsx +++ b/dapp/src/components/Header/ConnectWallet.tsx @@ -1,10 +1,6 @@ import React from "react" import { Button, HStack, Icon, Tooltip } from "@chakra-ui/react" -import { - useRequestBitcoinAccount, - useRequestEthereumAccount, - useWalletContext, -} from "#/hooks" +import { useWallet } from "#/hooks" import { CurrencyBalance } from "#/components/shared/CurrencyBalance" import { TextMd } from "#/components/shared/Typography" import { BitcoinIcon, EthereumIcon } from "#/assets/icons" @@ -30,9 +26,10 @@ const getCustomDataByAccount = ( } export default function ConnectWallet() { - const { requestAccount: requestBitcoinAccount } = useRequestBitcoinAccount() - const { requestAccount: requestEthereumAccount } = useRequestEthereumAccount() - const { btcAccount, ethAccount } = useWalletContext() + const { + bitcoin: { account: btcAccount, requestAccount: requestBitcoinAccount }, + ethereum: { account: ethAccount, requestAccount: requestEthereumAccount }, + } = useWallet() const customDataBtcAccount = getCustomDataByAccount(btcAccount) const customDataEthAccount = getCustomDataByAccount(ethAccount) diff --git a/dapp/src/components/LiquidStakingTokenPopover.tsx b/dapp/src/components/LiquidStakingTokenPopover.tsx index 229a37ffa..b16da54c0 100644 --- a/dapp/src/components/LiquidStakingTokenPopover.tsx +++ b/dapp/src/components/LiquidStakingTokenPopover.tsx @@ -12,8 +12,8 @@ import { import { SizeType } from "#/types" import { useDocsDrawer, useSharesBalance, useWalletContext } from "#/hooks" import { TextMd, TextSm } from "./shared/Typography" -import Alert from "./shared/Alert" import { CurrencyBalance } from "./shared/CurrencyBalance" +import { CardAlert } from "./shared/alerts" type LiquidStakingTokenPopoverProps = PopoverProps & { popoverSize: SizeType } @@ -55,18 +55,18 @@ export function LiquidStakingTokenPopover({ variant="greater-balance-xl" currency="stbtc" /> - Your tokens are this Ethereum address once the staking transaction is finalized. - + diff --git a/dapp/src/components/TransactionHistory/Table/utils/columns.tsx b/dapp/src/components/TransactionHistory/Table/utils/columns.tsx index 7523e56f8..d656a50ea 100644 --- a/dapp/src/components/TransactionHistory/Table/utils/columns.tsx +++ b/dapp/src/components/TransactionHistory/Table/utils/columns.tsx @@ -1,7 +1,7 @@ import React from "react" import { ColumnDef, createColumnHelper } from "@tanstack/react-table" import { StakeHistory } from "#/types" -import { capitalize, truncateAddress } from "#/utils" +import { capitalizeFirstLetter, truncateAddress } from "#/utils" import CustomCell from "../Cell/Custom" import Cell from "../Cell" import SimpleText from "../Cell/components/SimpleText" @@ -32,10 +32,14 @@ export const COLUMNS: ColumnDef[] = [ cell: ({ row: { original } }) => ( {capitalize(original.callTx.action)} + + {capitalizeFirstLetter(original.callTx.action)} + } secondField={ - {capitalize(original.receiptTx.action)} + + {capitalizeFirstLetter(original.receiptTx.action)} + } /> ), diff --git a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx index f5d21eb54..4b68afa54 100644 --- a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx @@ -1,29 +1,39 @@ -import React, { useCallback } from "react" +import React, { useCallback, useState } from "react" import { useDepositBTCTransaction, useDepositTelemetry, useExecuteFunction, useModalFlowContext, useStakeFlowContext, + useToast, useTransactionContext, useWalletContext, } from "#/hooks" -import Alert from "#/components/shared/Alert" import { TextMd } from "#/components/shared/Typography" import { logPromiseFailure } from "#/utils" import { PROCESS_STATUSES } from "#/types" +import { CardAlert } from "#/components/shared/alerts" +import { TOASTS, TOAST_IDS } from "#/types/toast" import StakingStepsModalContent from "./StakingStepsModalContent" +const TOAST_ID = TOAST_IDS.DEPOSIT_TRANSACTION_ERROR +const TOAST = TOASTS[TOAST_ID] + export default function DepositBTCModal() { const { ethAccount } = useWalletContext() const { tokenAmount } = useTransactionContext() const { setStatus } = useModalFlowContext() const { btcAddress, depositReceipt, stake } = useStakeFlowContext() const depositTelemetry = useDepositTelemetry() + const { closeToast, openToast } = useToast() - const onStakeBTCSuccess = useCallback(() => { - setStatus(PROCESS_STATUSES.SUCCEEDED) - }, [setStatus]) + const [isLoading, setIsLoading] = useState(false) + const [buttonText, setButtonText] = useState("Deposit BTC") + + const onStakeBTCSuccess = useCallback( + () => setStatus(PROCESS_STATUSES.SUCCEEDED), + [setStatus], + ) const onStakeBTCError = useCallback(() => { setStatus(PROCESS_STATUSES.FAILED) @@ -36,34 +46,51 @@ export default function DepositBTCModal() { ) const onDepositBTCSuccess = useCallback(() => { + closeToast(TOAST_ID) setStatus(PROCESS_STATUSES.LOADING) logPromiseFailure(handleStake()) - }, [setStatus, handleStake]) + }, [closeToast, setStatus, handleStake]) - const { sendBitcoinTransaction } = - useDepositBTCTransaction(onDepositBTCSuccess) + const showError = useCallback(() => { + openToast({ + id: TOAST_ID, + render: TOAST, + }) + setButtonText("Try again") + }, [openToast]) + + const onDepositBTCError = useCallback(() => showError(), [showError]) + + const { sendBitcoinTransaction } = useDepositBTCTransaction( + onDepositBTCSuccess, + onDepositBTCError, + ) const handledDepositBTC = useCallback(async () => { if (!tokenAmount?.amount || !btcAddress || !depositReceipt || !ethAccount) return + setIsLoading(true) const response = await depositTelemetry( depositReceipt, btcAddress, ethAccount.address, ) + setIsLoading(false) - // TODO: Display the correct message for the user - if (response.verificationStatus !== "valid") return - - logPromiseFailure(sendBitcoinTransaction(tokenAmount?.amount, btcAddress)) + if (response.verificationStatus === "valid") { + logPromiseFailure(sendBitcoinTransaction(tokenAmount?.amount, btcAddress)) + } else { + showError() + } }, [ btcAddress, depositReceipt, depositTelemetry, ethAccount, sendBitcoinTransaction, + showError, tokenAmount?.amount, ]) @@ -73,15 +100,16 @@ export default function DepositBTCModal() { return ( - + Make a Bitcoin transaction to deposit and stake your BTC. - + ) } diff --git a/dapp/src/components/TransactionModal/ActiveStakingStep/SignMessageModal.tsx b/dapp/src/components/TransactionModal/ActiveStakingStep/SignMessageModal.tsx index 84803da08..37bba6883 100644 --- a/dapp/src/components/TransactionModal/ActiveStakingStep/SignMessageModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveStakingStep/SignMessageModal.tsx @@ -1,34 +1,58 @@ -import React, { useCallback, useEffect } from "react" +import React, { useCallback, useEffect, useState } from "react" import { useExecuteFunction, useModalFlowContext, useStakeFlowContext, + useToast, } from "#/hooks" import { logPromiseFailure } from "#/utils" -import AlertReceiveSTBTC from "#/components/shared/AlertReceiveSTBTC" -import { PROCESS_STATUSES } from "#/types" +import { PROCESS_STATUSES, TOASTS, TOAST_IDS } from "#/types" +import { ReceiveSTBTCAlert } from "#/components/shared/alerts" import StakingStepsModalContent from "./StakingStepsModalContent" +const TOAST_ID = TOAST_IDS.SIGNING_ERROR +const TOAST = TOASTS[TOAST_ID] + export default function SignMessageModal() { const { goNext, setStatus } = useModalFlowContext() const { signMessage } = useStakeFlowContext() - const handleSignMessage = useExecuteFunction(signMessage, goNext) - - const handleSignMessageWrapper = useCallback(() => { - logPromiseFailure(handleSignMessage()) - }, [handleSignMessage]) + const [buttonText, setButtonText] = useState("Sign now") + const { closeToast, openToast } = useToast() useEffect(() => { setStatus(PROCESS_STATUSES.PENDING) }, [setStatus]) + const onSignMessageSuccess = useCallback(() => { + closeToast(TOAST_ID) + goNext() + }, [closeToast, goNext]) + + const onSignMessageError = useCallback(() => { + openToast({ + id: TOAST_ID, + render: TOAST, + }) + setButtonText("Try again") + }, [openToast]) + + const handleSignMessage = useExecuteFunction( + signMessage, + onSignMessageSuccess, + onSignMessageError, + ) + + const handleSignMessageWrapper = useCallback(() => { + logPromiseFailure(handleSignMessage()) + }, [handleSignMessage]) + return ( - + ) } diff --git a/dapp/src/components/TransactionModal/ActiveStakingStep/StakingStepsModalContent.tsx b/dapp/src/components/TransactionModal/ActiveStakingStep/StakingStepsModalContent.tsx index bca9915d2..24f13b273 100644 --- a/dapp/src/components/TransactionModal/ActiveStakingStep/StakingStepsModalContent.tsx +++ b/dapp/src/components/TransactionModal/ActiveStakingStep/StakingStepsModalContent.tsx @@ -1,14 +1,9 @@ import React from "react" -import { - Button, - HStack, - ModalBody, - ModalFooter, - ModalHeader, -} from "@chakra-ui/react" +import { HStack, ModalBody, ModalFooter, ModalHeader } from "@chakra-ui/react" import { TextLg, TextMd } from "#/components/shared/Typography" import StepperBase, { StepBase } from "#/components/shared/StepperBase" import Spinner from "#/components/shared/Spinner" +import { LoadingButton } from "#/components/shared/LoadingButton" export function Title({ children }: { children: React.ReactNode }) { return {children} @@ -43,12 +38,14 @@ const STEPS: StepBase[] = [ export default function StakingStepsModalContent({ buttonText, + isLoading, activeStep, onClick, children, }: { buttonText: string activeStep: number + isLoading?: boolean onClick: () => void children: React.ReactNode }) { @@ -68,9 +65,14 @@ export default function StakingStepsModalContent({ {children} - + ) diff --git a/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx b/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx index 0f9bd1003..d462cab94 100644 --- a/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx @@ -3,8 +3,8 @@ import { useExecuteFunction, useModalFlowContext } from "#/hooks" import { PROCESS_STATUSES } from "#/types" import { Button, ModalBody, ModalFooter, ModalHeader } from "@chakra-ui/react" import { TextMd } from "#/components/shared/Typography" -import AlertReceiveSTBTC from "#/components/shared/AlertReceiveSTBTC" import { logPromiseFailure } from "#/utils" +import { ReceiveSTBTCAlert } from "#/components/shared/alerts" export default function SignMessageModal() { const { setStatus } = useModalFlowContext() @@ -46,7 +46,7 @@ export default function SignMessageModal() { You will sign a gas-free Ethereum message to indicate the address where you'd like to get your stBTC liquid staking token. - + + ) } diff --git a/dapp/src/components/shared/LoadingButton.tsx b/dapp/src/components/shared/LoadingButton.tsx new file mode 100644 index 000000000..aa8c1715d --- /dev/null +++ b/dapp/src/components/shared/LoadingButton.tsx @@ -0,0 +1,10 @@ +import React from "react" +import { Button, ButtonProps, Spinner } from "@chakra-ui/react" + +export function LoadingButton({ isLoading, children, ...props }: ButtonProps) { + return ( + + ) +} diff --git a/dapp/src/components/shared/ModalBase/index.tsx b/dapp/src/components/shared/ModalBase/index.tsx index 13731579c..416b22850 100644 --- a/dapp/src/components/shared/ModalBase/index.tsx +++ b/dapp/src/components/shared/ModalBase/index.tsx @@ -5,7 +5,7 @@ export const MODAL_BASE_SIZE = "lg" export default function ModalBase({ children, ...restProps }: ModalProps) { return ( - + {children} diff --git a/dapp/src/components/shared/Toast.tsx b/dapp/src/components/shared/Toast.tsx new file mode 100644 index 000000000..f341cbe8a --- /dev/null +++ b/dapp/src/components/shared/Toast.tsx @@ -0,0 +1,32 @@ +import React from "react" +import { HStack } from "@chakra-ui/react" +import { Alert, AlertProps } from "./alerts" +import { TextSm } from "./Typography" + +type ToastProps = { + title: string + subtitle?: string +} & Omit & { + onClose: () => void + } + +export default function Toast({ + title, + subtitle, + children, + onClose, + ...props +}: ToastProps) { + return ( + // Chakra UI uses an alert component for toast under the hood. + // Therefore, to define custom styles for the Toast component, + // need to base it on the Alert component. + + + {title} + {subtitle && {subtitle}} + {children} + + + ) +} diff --git a/dapp/src/components/shared/TokenBalanceInput/index.tsx b/dapp/src/components/shared/TokenBalanceInput/index.tsx index f9edbd942..2f3a89ff8 100644 --- a/dapp/src/components/shared/TokenBalanceInput/index.tsx +++ b/dapp/src/components/shared/TokenBalanceInput/index.tsx @@ -17,8 +17,8 @@ import { getCurrencyByType, userAmountToBigInt, } from "#/utils" -import { AlertInfo } from "#/assets/icons" import { CurrencyType } from "#/types" +import { IconInfoCircle } from "@tabler/icons-react" import NumberFormatInput, { NumberFormatInputValues, } from "../NumberFormatInput" @@ -48,7 +48,7 @@ function HelperErrorText({ if (helperText) { return ( - + {helperText} ) diff --git a/dapp/src/components/shared/alerts/Alert.tsx b/dapp/src/components/shared/alerts/Alert.tsx new file mode 100644 index 000000000..b22f08562 --- /dev/null +++ b/dapp/src/components/shared/alerts/Alert.tsx @@ -0,0 +1,47 @@ +import React from "react" +import { + Alert as ChakraAlert, + AlertProps as ChakraAlertProps, + Icon, + CloseButton, + HStack, +} from "@chakra-ui/react" +import { IconInfoCircle, IconExclamationCircle } from "@tabler/icons-react" + +const ICONS = { + info: IconInfoCircle, + error: IconExclamationCircle, +} + +type AlertStatus = keyof typeof ICONS + +export type AlertProps = ChakraAlertProps & { + status?: AlertStatus + colorIcon?: string + withIcon?: boolean + withCloseButton?: boolean + icon?: typeof Icon + onClose?: () => void +} + +export function Alert({ + status = "info", + colorIcon, + withIcon, + children, + withCloseButton, + onClose, + ...props +}: AlertProps) { + return ( + + {withIcon && status && ( + + )} + + {children} + {withCloseButton && } + + + ) +} diff --git a/dapp/src/components/shared/alerts/CardAlert.tsx b/dapp/src/components/shared/alerts/CardAlert.tsx new file mode 100644 index 000000000..1c40f8a4e --- /dev/null +++ b/dapp/src/components/shared/alerts/CardAlert.tsx @@ -0,0 +1,51 @@ +import React from "react" +import { HStack, Flex } from "@chakra-ui/react" +import { ArrowUpRight } from "#/assets/icons" +import { Alert, AlertProps } from "./Alert" + +export type CardAlertProps = { + withLink?: boolean + onClick?: () => void +} & Omit + +export function CardAlert({ + withLink = false, + children, + onClick, + ...props +}: CardAlertProps) { + return ( + + + {children} + + {withLink && ( + + + + )} + + ) +} diff --git a/dapp/src/components/shared/AlertReceiveSTBTC/index.tsx b/dapp/src/components/shared/alerts/ReceiveSTBTCAlert.tsx similarity index 58% rename from dapp/src/components/shared/AlertReceiveSTBTC/index.tsx rename to dapp/src/components/shared/alerts/ReceiveSTBTCAlert.tsx index 8045ccce5..d873f70b3 100644 --- a/dapp/src/components/shared/AlertReceiveSTBTC/index.tsx +++ b/dapp/src/components/shared/alerts/ReceiveSTBTCAlert.tsx @@ -1,18 +1,18 @@ import React from "react" import { Highlight } from "@chakra-ui/react" -import { TextMd } from "#/components/shared/Typography" -import Alert, { AlertProps } from "#/components/shared/Alert" +import { CardAlert, CardAlertProps } from "./CardAlert" +import { TextMd } from "../Typography" -export default function AlertReceiveSTBTC({ ...restProps }: AlertProps) { +export function ReceiveSTBTCAlert({ ...restProps }: CardAlertProps) { return ( // TODO: Add the correct action after click - {}} {...restProps}> + You will receive stBTC liquid staking token at this Ethereum address once the staking transaction is completed. - + ) } diff --git a/dapp/src/components/shared/alerts/index.ts b/dapp/src/components/shared/alerts/index.ts new file mode 100644 index 000000000..79555f74d --- /dev/null +++ b/dapp/src/components/shared/alerts/index.ts @@ -0,0 +1,3 @@ +export * from "./Alert" +export * from "./CardAlert" +export * from "./ReceiveSTBTCAlert" diff --git a/dapp/src/components/toasts/DepositTransactionErrorToast.tsx b/dapp/src/components/toasts/DepositTransactionErrorToast.tsx new file mode 100644 index 000000000..6a2dce775 --- /dev/null +++ b/dapp/src/components/toasts/DepositTransactionErrorToast.tsx @@ -0,0 +1,17 @@ +import React from "react" +import Toast from "../shared/Toast" + +export function DepositTransactionErrorToast({ + onClose, +}: { + onClose: () => void +}) { + return ( + + ) +} diff --git a/dapp/src/components/toasts/SigningMessageErrorToast.tsx b/dapp/src/components/toasts/SigningMessageErrorToast.tsx new file mode 100644 index 000000000..2b22cbb1a --- /dev/null +++ b/dapp/src/components/toasts/SigningMessageErrorToast.tsx @@ -0,0 +1,13 @@ +import React from "react" +import Toast from "../shared/Toast" + +export function SigningMessageErrorToast({ onClose }: { onClose: () => void }) { + return ( + + ) +} diff --git a/dapp/src/components/toasts/WalletErrorToast.tsx b/dapp/src/components/toasts/WalletErrorToast.tsx new file mode 100644 index 000000000..f77eb37d0 --- /dev/null +++ b/dapp/src/components/toasts/WalletErrorToast.tsx @@ -0,0 +1,23 @@ +import React from "react" +import { Flex, Button } from "@chakra-ui/react" +import Toast from "../shared/Toast" + +export function WalletErrorToast({ + title, + onClick, + onClose, +}: { + title: string + onClick?: () => void + onClose: () => void +}) { + return ( + + + + + + ) +} diff --git a/dapp/src/components/toasts/index.ts b/dapp/src/components/toasts/index.ts new file mode 100644 index 000000000..4fb789e27 --- /dev/null +++ b/dapp/src/components/toasts/index.ts @@ -0,0 +1,3 @@ +export * from "./DepositTransactionErrorToast" +export * from "./SigningMessageErrorToast" +export * from "./WalletErrorToast" diff --git a/dapp/src/hooks/index.ts b/dapp/src/hooks/index.ts index a443f5ae5..5651557f2 100644 --- a/dapp/src/hooks/index.ts +++ b/dapp/src/hooks/index.ts @@ -1,4 +1,5 @@ export * from "./store" +export * from "./toasts" export * from "./sdk" export * from "./useDetectThemeMode" export * from "./useRequestBitcoinAccount" @@ -17,6 +18,8 @@ export * from "./useInitApp" export * from "./useCurrencyConversion" export * from "./useDepositTelemetry" export * from "./useFetchBTCPriceUSD" +export * from "./useWallet" +export * from "./useTimeout" export * from "./useCountdown" export * from "./useActivities" export * from "./useSize" diff --git a/dapp/src/hooks/toasts/index.ts b/dapp/src/hooks/toasts/index.ts new file mode 100644 index 000000000..c9a093219 --- /dev/null +++ b/dapp/src/hooks/toasts/index.ts @@ -0,0 +1,3 @@ +export * from "./useInitGlobalToasts" +export * from "./useShowWalletErrorToast" +export * from "./useToast" diff --git a/dapp/src/hooks/toasts/useInitGlobalToasts.ts b/dapp/src/hooks/toasts/useInitGlobalToasts.ts new file mode 100644 index 000000000..0fec6ef12 --- /dev/null +++ b/dapp/src/hooks/toasts/useInitGlobalToasts.ts @@ -0,0 +1,6 @@ +import { useShowWalletErrorToast } from "./useShowWalletErrorToast" + +export function useInitGlobalToasts() { + useShowWalletErrorToast("ethereum") + useShowWalletErrorToast("bitcoin") +} diff --git a/dapp/src/hooks/toasts/useShowWalletErrorToast.ts b/dapp/src/hooks/toasts/useShowWalletErrorToast.ts new file mode 100644 index 000000000..df57b308c --- /dev/null +++ b/dapp/src/hooks/toasts/useShowWalletErrorToast.ts @@ -0,0 +1,59 @@ +import { useCallback, useEffect } from "react" +import { ONE_SEC_IN_MILLISECONDS } from "#/constants" +import { capitalizeFirstLetter, logPromiseFailure } from "#/utils" +import { TOASTS, TOAST_IDS } from "#/types" +import { useToast } from "./useToast" +import { useWallet } from "../useWallet" +import { useTimeout } from "../useTimeout" + +const { BITCOIN_WALLET_ERROR, ETHEREUM_WALLET_ERROR } = TOAST_IDS + +const WALLET_ERROR_TOAST_ID = { + bitcoin: { + id: BITCOIN_WALLET_ERROR, + Component: TOASTS[BITCOIN_WALLET_ERROR], + }, + ethereum: { + id: ETHEREUM_WALLET_ERROR, + Component: TOASTS[ETHEREUM_WALLET_ERROR], + }, +} + +export function useShowWalletErrorToast( + type: "bitcoin" | "ethereum", + delay = ONE_SEC_IN_MILLISECONDS, +) { + const { + [type]: { account, requestAccount }, + } = useWallet() + const { closeToast, openToast } = useToast() + + const { id, Component } = WALLET_ERROR_TOAST_ID[type] + + const handleConnect = useCallback( + () => logPromiseFailure(requestAccount()), + [requestAccount], + ) + + const handleOpen = useCallback( + () => + openToast({ + id, + render: ({ onClose }) => + Component({ + title: capitalizeFirstLetter(`${type} wallet is not connected`), + onClose, + onClick: handleConnect, + }), + }), + [Component, handleConnect, id, openToast, type], + ) + + useTimeout(handleOpen, delay) + + useEffect(() => { + if (!account) return + + closeToast(id) + }, [account, closeToast, id]) +} diff --git a/dapp/src/hooks/toasts/useToast.ts b/dapp/src/hooks/toasts/useToast.ts new file mode 100644 index 000000000..b38f1e098 --- /dev/null +++ b/dapp/src/hooks/toasts/useToast.ts @@ -0,0 +1,41 @@ +import { UseToastOptions, useToast as useChakraToast } from "@chakra-ui/react" +import { useCallback, useMemo } from "react" + +export function useToast() { + const toast = useChakraToast() + + const overriddenToast = useCallback( + (options: UseToastOptions) => + toast({ + position: "top", + duration: null, + isClosable: true, + containerStyle: { my: 1 }, + ...options, + }), + [toast], + ) + + const openToast = useCallback( + ({ id, ...options }: UseToastOptions) => { + if (!id) { + overriddenToast(options) + } else if (!toast.isActive(id)) { + overriddenToast({ + id, + ...options, + }) + } + }, + [overriddenToast, toast], + ) + + return useMemo(() => { + const { close: closeToast, ...rest } = Object.assign(overriddenToast, toast) + return { + ...rest, + openToast, + closeToast, + } + }, [overriddenToast, toast, openToast]) +} diff --git a/dapp/src/hooks/useDepositBTCTransaction.ts b/dapp/src/hooks/useDepositBTCTransaction.ts index d0296298a..f256255d2 100644 --- a/dapp/src/hooks/useDepositBTCTransaction.ts +++ b/dapp/src/hooks/useDepositBTCTransaction.ts @@ -63,10 +63,19 @@ export function useDepositBTCTransaction( await signAndBroadcastTransaction(btcAccount.id, bitcoinTransaction) walletApiReactTransport.disconnect() } catch (e) { + if (onError) { + onError(error) + } console.error(e) } }, - [btcAccount, signAndBroadcastTransaction, walletApiReactTransport], + [ + btcAccount, + error, + onError, + signAndBroadcastTransaction, + walletApiReactTransport, + ], ) return { ...rest, sendBitcoinTransaction, transactionHash, error } diff --git a/dapp/src/hooks/useInitApp.ts b/dapp/src/hooks/useInitApp.ts index 250c784af..dcdf70a92 100644 --- a/dapp/src/hooks/useInitApp.ts +++ b/dapp/src/hooks/useInitApp.ts @@ -1,6 +1,7 @@ import { useInitDataFromSdk, useInitializeAcreSdk } from "./sdk" import { useSentry } from "./sentry" import { useFetchBTCPriceUSD } from "./useFetchBTCPriceUSD" +import { useInitGlobalToasts } from "./toasts/useInitGlobalToasts" export function useInitApp() { // TODO: Let's uncomment when dark mode is ready @@ -9,4 +10,5 @@ export function useInitApp() { useInitializeAcreSdk() useInitDataFromSdk() useFetchBTCPriceUSD() + useInitGlobalToasts() } diff --git a/dapp/src/hooks/useTimeout.ts b/dapp/src/hooks/useTimeout.ts new file mode 100644 index 000000000..ba037809e --- /dev/null +++ b/dapp/src/hooks/useTimeout.ts @@ -0,0 +1,25 @@ +import { useEffect, useLayoutEffect, useRef } from "react" + +// Source: https://usehooks-ts.com/react-hook/use-timeout +export function useTimeout(callback: () => void, delay: number | null) { + const savedCallback = useRef(callback) + + // Remember the latest callback if it changes. + useLayoutEffect(() => { + savedCallback.current = callback + }, [callback]) + + // Set up the timeout. + useEffect(() => { + // Don't schedule if no delay is specified. + // Note: 0 is a valid value for delay. + if (!delay && delay !== 0) { + return + } + + const id = setTimeout(() => savedCallback.current(), delay) + + // eslint-disable-next-line consistent-return + return () => clearTimeout(id) + }, [delay]) +} diff --git a/dapp/src/hooks/useWallet.ts b/dapp/src/hooks/useWallet.ts new file mode 100644 index 000000000..7511494d6 --- /dev/null +++ b/dapp/src/hooks/useWallet.ts @@ -0,0 +1,18 @@ +import { useMemo } from "react" +import { useWalletContext } from "./useWalletContext" +import { useRequestBitcoinAccount } from "./useRequestBitcoinAccount" +import { useRequestEthereumAccount } from "./useRequestEthereumAccount" + +export function useWallet() { + const { btcAccount, ethAccount } = useWalletContext() + const { requestAccount: requestBitcoinAccount } = useRequestBitcoinAccount() + const { requestAccount: requestEthereumAccount } = useRequestEthereumAccount() + + return useMemo( + () => ({ + bitcoin: { account: btcAccount, requestAccount: requestBitcoinAccount }, + ethereum: { account: ethAccount, requestAccount: requestEthereumAccount }, + }), + [btcAccount, requestBitcoinAccount, ethAccount, requestEthereumAccount], + ) +} diff --git a/dapp/src/theme/Alert.ts b/dapp/src/theme/Alert.ts index c3b42c09f..cb002ea56 100644 --- a/dapp/src/theme/Alert.ts +++ b/dapp/src/theme/Alert.ts @@ -6,50 +6,41 @@ import { defineStyle, } from "@chakra-ui/react" -const KEYS = [...parts.keys, "rightIconContainer"] as const - const baseStyleDialog = defineStyle({ - py: 5, - pl: 5, - pr: 10, - border: "2px", - borderColor: "white", + px: 5, borderRadius: "xl", textAlign: "left", - color: "grey.700", + color: "white", + width: "lg", + boxShadow: "0px 8px 12px 0px var(--chakra-colors-opacity-black-15)", }) const baseStyleIcon = defineStyle({ - mr: 4, -}) - -const baseStyleRightIconContainer = defineStyle({ - position: "absolute", - right: 0, - top: 0, - p: 5, - h: "100%", - borderLeft: "2px solid white", - color: "brand.400", - display: "flex", - alignItems: "center", - w: 14, + mr: 2, }) -const multiStyleConfig = createMultiStyleConfigHelpers(KEYS) +const multiStyleConfig = createMultiStyleConfigHelpers(parts.keys) const baseStyle = multiStyleConfig.definePartsStyle({ container: baseStyleDialog, icon: baseStyleIcon, - rightIconContainer: baseStyleRightIconContainer, }) const statusInfo = multiStyleConfig.definePartsStyle({ container: { - bg: "gold.200", + bg: "blue.500", + }, + icon: { + color: "white", + }, +}) + +const statusError = multiStyleConfig.definePartsStyle({ + container: { + bg: "red.400", }, icon: { - color: "grey.700", + color: "white", }, }) @@ -60,6 +51,7 @@ const statusStyles = (props: StyleFunctionProps) => { [status: string]: Record } = { info: statusInfo, + error: statusError, } return styleMap[status as string] || {} diff --git a/dapp/src/theme/Button.ts b/dapp/src/theme/Button.ts index 3174f12df..ccb108bd7 100644 --- a/dapp/src/theme/Button.ts +++ b/dapp/src/theme/Button.ts @@ -27,6 +27,12 @@ export const buttonTheme: ComponentSingleStyleConfig = { _active: { bg: "brand.400", }, + _loading: { + _disabled: { + background: "gold.300", + opacity: 1, + }, + }, }, outline: ({ colorScheme }: StyleFunctionProps) => { const defaultStyles = { @@ -60,6 +66,18 @@ export const buttonTheme: ComponentSingleStyleConfig = { }, } } + + if (colorScheme === "white") { + return { + ...defaultStyles, + color: "white", + borderColor: "white", + + _hover: { + bg: "opacity.black.05", + }, + } + } return defaultStyles }, ghost: { diff --git a/dapp/src/theme/Modal.ts b/dapp/src/theme/Modal.ts index 2c2d1d158..8436cc293 100644 --- a/dapp/src/theme/Modal.ts +++ b/dapp/src/theme/Modal.ts @@ -13,8 +13,15 @@ const baseStyleDialog = defineStyle({ const baseCloseButton = defineStyle({ top: -10, right: -10, + height: 7, + width: 7, + p: 1.5, rounded: "100%", bg: "opacity.white.5", + + _hover: { + bg: "opacity.white.5", + }, }) const baseStyleOverlay = defineStyle({ diff --git a/dapp/src/theme/index.ts b/dapp/src/theme/index.ts index 4b7989273..add79614a 100644 --- a/dapp/src/theme/index.ts +++ b/dapp/src/theme/index.ts @@ -1,8 +1,14 @@ -import { StyleFunctionProps, extendTheme } from "@chakra-ui/react" -import { mode } from "@chakra-ui/theme-tools" +import { extendTheme } from "@chakra-ui/react" import { buttonTheme } from "./Button" import { switchTheme } from "./Switch" -import { colors, fonts, lineHeights, semanticTokens, zIndices } from "./utils" +import { + colors, + fonts, + lineHeights, + semanticTokens, + styles, + zIndices, +} from "./utils" import { drawerTheme } from "./Drawer" import { modalTheme } from "./Modal" import { cardTheme } from "./Card" @@ -38,15 +44,7 @@ const defaultTheme = { lineHeights, zIndices, semanticTokens, - styles: { - global: (props: StyleFunctionProps) => ({ - body: { - // TODO: Update when the dark theme is ready - backgroundColor: mode("gold.300", "gold.300")(props), - color: mode("grey.700", "grey.700")(props), - }, - }), - }, + styles, components: { Alert: alertTheme, Button: buttonTheme, diff --git a/dapp/src/theme/utils/colors.ts b/dapp/src/theme/utils/colors.ts index eea473462..14a77fc45 100644 --- a/dapp/src/theme/utils/colors.ts +++ b/dapp/src/theme/utils/colors.ts @@ -63,5 +63,9 @@ export const colors = { "05": "rgba(35, 31, 32, 0.05)", }, }, + black: { + "05": "rgba(0, 0, 0, 0.05)", + 15: "rgba(0, 0, 0, 0.15)", + }, }, } diff --git a/dapp/src/theme/utils/index.ts b/dapp/src/theme/utils/index.ts index c7cef473b..12228cd98 100644 --- a/dapp/src/theme/utils/index.ts +++ b/dapp/src/theme/utils/index.ts @@ -2,4 +2,5 @@ export * from "./colors" export * from "./fonts" export * from "./zIndices" export * from "./semanticTokens" +export * from "./styles" export * from "./units" diff --git a/dapp/src/theme/utils/semanticTokens.ts b/dapp/src/theme/utils/semanticTokens.ts index ccdf4561b..c25ccd8b7 100644 --- a/dapp/src/theme/utils/semanticTokens.ts +++ b/dapp/src/theme/utils/semanticTokens.ts @@ -1,7 +1,8 @@ export const semanticTokens = { space: { header_height: 24, - modal_shift: 36, + modal_shift: 48, + toast_container_shift: 16, modal_borderWidth: "2px", }, sizes: { diff --git a/dapp/src/theme/utils/styles.ts b/dapp/src/theme/utils/styles.ts new file mode 100644 index 000000000..a1a891663 --- /dev/null +++ b/dapp/src/theme/utils/styles.ts @@ -0,0 +1,28 @@ +import { StyleFunctionProps, defineStyle } from "@chakra-ui/react" +import { mode } from "@chakra-ui/theme-tools" +import { semanticTokens } from "./semanticTokens" +import { zIndices } from "./zIndices" + +const bodyStyle = defineStyle((props: StyleFunctionProps) => ({ + // TODO: Update when the dark theme is ready + backgroundColor: mode("gold.300", "gold.300")(props), + color: mode("grey.700", "grey.700")(props), +})) + +const toastManagerTopStyle = defineStyle({ + marginTop: `${semanticTokens.space.toast_container_shift} !important`, + // To set the correct z-index value for the toast component, + // we need to override it in the global styles + // More info: + // https://github.com/chakra-ui/chakra-ui/issues/7505 + zIndex: `${zIndices.toast} !important`, +}) + +const globalStyle = (props: StyleFunctionProps) => ({ + "#chakra-toast-manager-top": toastManagerTopStyle, + body: bodyStyle(props), +}) + +export const styles = { + global: (props: StyleFunctionProps) => globalStyle(props), +} diff --git a/dapp/src/theme/utils/zIndices.ts b/dapp/src/theme/utils/zIndices.ts index a0834c287..4eda9c324 100644 --- a/dapp/src/theme/utils/zIndices.ts +++ b/dapp/src/theme/utils/zIndices.ts @@ -1,4 +1,5 @@ export const zIndices = { sidebar: 1450, drawer: 1470, + toast: 1480, } diff --git a/dapp/src/types/index.ts b/dapp/src/types/index.ts index 1c99facdd..16d93da3b 100644 --- a/dapp/src/types/index.ts +++ b/dapp/src/types/index.ts @@ -12,3 +12,4 @@ export * from "./activity" export * from "./coingecko" export * from "./time" export * from "./size" +export * from "./toast" diff --git a/dapp/src/types/toast.ts b/dapp/src/types/toast.ts new file mode 100644 index 000000000..c4a3fe962 --- /dev/null +++ b/dapp/src/types/toast.ts @@ -0,0 +1,23 @@ +import { ReactNode } from "react" +import { + DepositTransactionErrorToast, + SigningMessageErrorToast, + WalletErrorToast, +} from "#/components/toasts" + +export const TOAST_IDS = { + BITCOIN_WALLET_ERROR: "bitcoin-wallet-error", + ETHEREUM_WALLET_ERROR: "ethereum-wallet-error", + SIGNING_ERROR: "signing-error", + DEPOSIT_TRANSACTION_ERROR: "deposit-transaction-error", +} as const + +export type ToastID = (typeof TOAST_IDS)[keyof typeof TOAST_IDS] + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const TOASTS: Record ReactNode> = { + [TOAST_IDS.BITCOIN_WALLET_ERROR]: WalletErrorToast, + [TOAST_IDS.ETHEREUM_WALLET_ERROR]: WalletErrorToast, + [TOAST_IDS.SIGNING_ERROR]: SigningMessageErrorToast, + [TOAST_IDS.DEPOSIT_TRANSACTION_ERROR]: DepositTransactionErrorToast, +} diff --git a/dapp/src/utils/text.ts b/dapp/src/utils/text.ts index 1b8b48f91..36e034408 100644 --- a/dapp/src/utils/text.ts +++ b/dapp/src/utils/text.ts @@ -1,2 +1,2 @@ -export const capitalize = (text: string): string => +export const capitalizeFirstLetter = (text: string): string => text[0].toUpperCase() + text.slice(1)