Mp 3413 pending tx toasts (#478)

* MP-3413: added pending transaction toast

* fix: removed txLoader/broadcastInitialized

* fix: replace the toast instead of closing the previous

* fix: fixed the build

* MP-3413: added transition on update and success checkmark

* fix: changed Search for ChevronDown
This commit is contained in:
Linkie Link 2023-09-18 16:54:36 +02:00 committed by GitHub
parent b04f244d3e
commit 74127213aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 570 additions and 678 deletions

View File

@ -36,7 +36,7 @@ export default function AccountFundContent(props: Props) {
const deposit = useStore((s) => s.deposit)
const accounts = useStore((s) => s.accounts)
const walletAssetModal = useStore((s) => s.walletAssetsModal)
const showTxLoader = useStore((s) => s.showTxLoader)
const [isConfirming, setIsConfirming] = useState(false)
const [lendAssets, setLendAssets] = useLocalStorage<boolean>(
LEND_ASSETS_KEY,
DEFAULT_SETTINGS.lendAssets,
@ -76,18 +76,27 @@ export default function AccountFundContent(props: Props) {
const handleClick = useCallback(async () => {
if (!props.accountId) return
const result = await deposit({
const depositObject = {
accountId: props.accountId,
coins: fundingAssets,
lend: isLending,
})
if (result)
useStore.setState({
fundAndWithdrawModal: null,
walletAssetsModal: null,
focusComponent: null,
})
}, [props.accountId, deposit, fundingAssets, isLending])
}
if (props.isFullPage) {
setIsConfirming(true)
const result = await deposit(depositObject)
setIsConfirming(false)
if (result)
useStore.setState({
walletAssetsModal: null,
focusComponent: null,
})
} else {
deposit(depositObject)
useStore.setState({ fundAndWithdrawModal: null, walletAssetsModal: null })
}
}, [props.accountId, deposit, fundingAssets, isLending, props.isFullPage])
useEffect(() => {
if (BN(baseBalance).isLessThan(defaultFee.amount[0].amount)) {
@ -171,7 +180,7 @@ export default function AccountFundContent(props: Props) {
max={balance}
balances={balances}
maxText='Max'
disabled={showTxLoader}
disabled={isConfirming}
/>
</div>
)
@ -184,7 +193,7 @@ export default function AccountFundContent(props: Props) {
rightIcon={<Plus />}
iconClassName='w-3'
onClick={handleSelectAssetsClick}
disabled={showTxLoader}
disabled={isConfirming}
/>
<DepositCapMessage
action='fund'
@ -203,7 +212,7 @@ export default function AccountFundContent(props: Props) {
className='w-full mt-4'
text='Fund account'
disabled={!hasFundingAssets || depositCapReachedCoins.length > 0}
showProgressIndicator={showTxLoader}
showProgressIndicator={isConfirming}
onClick={handleClick}
color={props.isFullPage ? 'tertiary' : undefined}
size={props.isFullPage ? 'lg' : undefined}

View File

@ -57,7 +57,7 @@ export default function AccountMenuContent(props: Props) {
}, [transactionFeeCoinBalance])
const performCreateAccount = useCallback(async () => {
setShowMenu(true)
setShowMenu(false)
setIsCreating(true)
const accountId = await createAccount()
setIsCreating(false)

View File

@ -0,0 +1,55 @@
import classNames from 'classnames'
import { CheckCircled } from 'components/Icons'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
interface Props {
color?: string
size?: number
className?: string
}
export const CheckMark = ({ color = '#FFFFFF', size = 20, className }: Props) => {
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
const classes = classNames('inline-block relative', className)
if (reduceMotion)
return (
<CheckCircled
className={classes}
style={{ width: `${size}px`, height: `${size}px`, color: `${color}` }}
/>
)
return (
<div className={classes} style={{ width: `${size}px`, height: `${size}px` }}>
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 130.2 130.2'>
<circle
className='animate-circle'
fill='none'
strokeDasharray='1000'
strokeDashoffset='0'
stroke={color}
strokeWidth='6'
strokeMiterlimit='10'
cx='65.1'
cy='65.1'
r='62.1'
/>
<polyline
className='animate-check'
fill='none'
strokeDasharray='1000'
strokeDashoffset='-100'
stroke={color}
strokeWidth='6'
strokeLinecap='round'
strokeMiterlimit='10'
points='100.2,40.2 51.5,88.8 29.8,67.5 '
/>
</svg>
</div>
)
}

View File

@ -1,6 +1,5 @@
import classNames from 'classnames'
import Text from 'components/Text'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
@ -25,9 +24,12 @@ export const CircularProgress = ({ color = '#FFFFFF', size = 20, className }: Pr
className={classNames('flex items-center', loaderClasses)}
style={{ width: `${size}px`, height: `${size}px` }}
>
<Text className='text-center' uppercase size='lg'>
<p
className='w-full text-center'
style={{ fontSize: `${size}px`, lineHeight: `${size}px`, color: `${color}` }}
>
...
</Text>
</p>
</div>
)

View File

@ -3,7 +3,6 @@ import { ReactNode, useEffect, useRef } from 'react'
import EscButton from 'components/Button/EscButton'
import Card from 'components/Card'
import TransactionLoader from 'components/TransactionLoader'
import useStore from 'store'
interface Props {
@ -23,11 +22,9 @@ interface Props {
export default function Modal(props: Props) {
const ref: React.RefObject<HTMLDialogElement> = useRef(null)
const modalClassName = props.modalClassName ?? 'max-w-modal'
const showTxLoader = useStore((s) => s.showTxLoader)
function onClose() {
ref.current?.close()
useStore.setState({ showTxLoader: false })
props.onClose()
}
@ -42,7 +39,6 @@ export default function Modal(props: Props) {
return () => {
dialog?.removeAttribute('open')
dialog?.close()
useStore.setState({ showTxLoader: false })
document.body.classList.remove('h-screen', 'overflow-hidden')
}
}, [])
@ -76,7 +72,6 @@ export default function Modal(props: Props) {
'flex-1 overflow-y-scroll scrollbar-hide relative',
)}
>
{showTxLoader && !props.hideTxLoader && <TransactionLoader />}
{props.children ? props.children : props.content}
</div>
</Card>

View File

@ -35,13 +35,11 @@ function AccountDeleteModal(props: Props) {
useStore.setState({ accountDeleteModal: null })
}, [])
const deleteAccountHandler = useCallback(async () => {
const deleteAccountHandler = useCallback(() => {
const options = { accountId: modal.id, lends: modal.lends }
const isSuccess = await deleteAccount(options)
if (isSuccess) {
navigate(getRoute(getPage(pathname), address))
closeDeleteAccountModal()
}
deleteAccount(options)
navigate(getRoute(getPage(pathname), address))
closeDeleteAccountModal()
}, [modal, deleteAccount, navigate, pathname, address, closeDeleteAccountModal])
const depositsAndLends = useMemo(
@ -92,7 +90,6 @@ function AccountDeleteModal(props: Props) {
positiveButton={{
text: 'Delete Account',
icon: <ArrowRight />,
isAsync: true,
onClick: deleteAccountHandler,
}}
/>
@ -120,7 +117,6 @@ function AccountDeleteModal(props: Props) {
positiveButton={{
text: 'Delete Account',
icon: <ArrowRight />,
isAsync: true,
onClick: deleteAccountHandler,
}}
/>

View File

@ -23,20 +23,11 @@ interface Props {
function AlertDialog(props: Props) {
const { title, icon, description, negativeButton, positiveButton } = props.config
const [isConfirming, setIsConfirming] = useState(false)
const handleButtonClick = (button?: AlertDialogButton) => {
button?.onClick && button.onClick()
props.close()
}
async function handleAsyncButtonClick(button?: AlertDialogButton) {
if (!button?.onClick) return
setIsConfirming(true)
await button.onClick()
setIsConfirming(false)
props.close()
}
return (
<Modal
onClose={props.close}
@ -66,12 +57,7 @@ function AlertDialog(props: Props) {
color='primary'
className='px-6'
rightIcon={positiveButton.icon ?? <YesIcon />}
showProgressIndicator={isConfirming}
onClick={() =>
positiveButton.isAsync
? handleAsyncButtonClick(positiveButton)
: handleButtonClick(positiveButton)
}
onClick={() => handleButtonClick(positiveButton)}
/>
)}
<Button
@ -79,7 +65,6 @@ function AlertDialog(props: Props) {
color='secondary'
className='px-6'
rightIcon={negativeButton?.icon ?? <NoIcon />}
disabled={isConfirming}
tabIndex={1}
onClick={() => handleButtonClick(negativeButton)}
/>

View File

@ -19,7 +19,6 @@ interface Props {
coinBalances: Coin[]
actionButtonText: string
contentHeader?: JSX.Element
showProgressIndicator: boolean
onClose: () => void
onChange: (value: BigNumber) => void
onAction: (value: BigNumber, isMax: boolean) => void
@ -32,7 +31,6 @@ export default function AssetAmountSelectActionModal(props: Props) {
coinBalances,
actionButtonText,
contentHeader = null,
showProgressIndicator,
onClose,
onChange,
onAction,
@ -77,12 +75,10 @@ export default function AssetAmountSelectActionModal(props: Props) {
max={maxAmount}
hasSelect
maxText='Max'
disabled={showProgressIndicator}
/>
<Divider />
<Button
onClick={handleActionClick}
showProgressIndicator={showProgressIndicator}
disabled={!amount.toNumber()}
className='w-full'
text={actionButtonText}

View File

@ -56,7 +56,6 @@ export default function BorrowModalController() {
function BorrowModal(props: Props) {
const { modal, account } = props
const [amount, setAmount] = useState(BN_ZERO)
const showTxLoader = useStore((s) => s.showTxLoader)
const [borrowToWallet, setBorrowToWallet] = useToggle()
const borrow = useStore((s) => s.borrow)
const repay = useStore((s) => s.repay)
@ -79,32 +78,29 @@ function BorrowModal(props: Props) {
setAmount(BN_ZERO)
}
async function onConfirmClick() {
function onConfirmClick() {
if (!asset) return
let result
const { lend } = getDepositAndLendCoinsToSpend(
BNCoin.fromDenomAndBigNumber(asset.denom, amount),
account,
)
if (isRepay) {
result = await repay({
repay({
accountId: account.id,
coin: BNCoin.fromDenomAndBigNumber(asset.denom, amount),
accountBalance: amount.isEqualTo(totalDebtRepayAmount),
lend,
})
} else {
result = await borrow({
borrow({
accountId: account.id,
coin: BNCoin.fromDenomAndBigNumber(asset.denom, amount),
borrowToWallet,
})
}
if (result) {
resetState()
useStore.setState({ borrowModal: null })
}
resetState()
useStore.setState({ borrowModal: null })
}
function onClose() {
@ -229,7 +225,6 @@ function BorrowModal(props: Props) {
max={max}
className='w-full'
maxText='Max'
disabled={showTxLoader}
/>
{!isRepay && (
<>
@ -245,7 +240,6 @@ function BorrowModal(props: Props) {
name='borrow-to-wallet'
checked={borrowToWallet}
onChange={setBorrowToWallet}
disabled={showTxLoader}
/>
</div>
</>
@ -254,7 +248,6 @@ function BorrowModal(props: Props) {
<Button
onClick={onConfirmClick}
className='w-full'
showProgressIndicator={showTxLoader}
disabled={amount.isZero()}
text={isRepay ? 'Repay' : 'Borrow'}
rightIcon={<ArrowRight />}

View File

@ -28,7 +28,6 @@ export default function WithdrawFromAccount(props: Props) {
ASSETS.find(byDenom(account.deposits[0]?.denom || account.lends[0]?.denom)) ?? ASSETS[0]
const withdraw = useStore((s) => s.withdraw)
const [withdrawWithBorrowing, setWithdrawWithBorrowing] = useToggle()
const showTxLoader = useStore((s) => s.showTxLoader)
const [currentAsset, setCurrentAsset] = useState(defaultAsset)
const [amount, setAmount] = useState(BN_ZERO)
const { simulateWithdraw } = useUpdatedAccount(account)
@ -56,13 +55,7 @@ export default function WithdrawFromAccount(props: Props) {
setAmount(val)
}
function resetState() {
setCurrentAsset(defaultAsset)
setAmount(BN_ZERO)
onChangeAmount(BN_ZERO)
}
async function onConfirm() {
function onConfirm() {
const coins = [
{
coin: BNCoin.fromDenomAndBigNumber(currentAsset.denom, amount),
@ -80,17 +73,13 @@ export default function WithdrawFromAccount(props: Props) {
]
: []
const result = await withdraw({
withdraw({
accountId: account.id,
coins,
borrow,
reclaims,
})
if (result) {
resetState()
useStore.setState({ fundAndWithdrawModal: null })
}
useStore.setState({ fundAndWithdrawModal: null })
}
useEffect(() => {
@ -123,7 +112,6 @@ export default function WithdrawFromAccount(props: Props) {
accountId={account.id}
hasSelect
maxText='Max'
disabled={showTxLoader}
/>
<Divider className='my-6' />
<div className='flex flex-wrap w-full'>
@ -138,18 +126,11 @@ export default function WithdrawFromAccount(props: Props) {
name='borrow-to-wallet'
checked={withdrawWithBorrowing}
onChange={setWithdrawWithBorrowing}
disabled={showTxLoader}
/>
</div>
</div>
</div>
<Button
onClick={onConfirm}
showProgressIndicator={showTxLoader}
className='w-full'
text={'Withdraw'}
rightIcon={<ArrowRight />}
/>
<Button onClick={onConfirm} className='w-full' text={'Withdraw'} rightIcon={<ArrowRight />} />
</>
)
}

View File

@ -26,7 +26,6 @@ function LendAndReclaimModal({ currentAccount, config }: Props) {
const lend = useStore((s) => s.lend)
const reclaim = useStore((s) => s.reclaim)
const { close } = useLendAndReclaimModal()
const showTxLoader = useStore((s) => s.showTxLoader)
const { simulateLending } = useUpdatedAccount(currentAccount)
const { data, action } = config
@ -45,14 +44,18 @@ function LendAndReclaimModal({ currentAccount, config }: Props) {
)
const handleAction = useCallback(
async (value: BigNumber, isMax: boolean) => {
(value: BigNumber, isMax: boolean) => {
const coin = BNCoin.fromDenomAndBigNumber(asset.denom, value)
const options = {
accountId: currentAccount.id,
coin,
isMax,
}
await (isLendAction ? lend : reclaim)(options)
if (isLendAction) {
lend(options)
} else {
reclaim(options)
}
close()
},
[asset.denom, close, currentAccount.id, isLendAction, lend, reclaim],
@ -63,7 +66,6 @@ function LendAndReclaimModal({ currentAccount, config }: Props) {
contentHeader={<DetailsHeader data={data} />}
coinBalances={coinBalances}
actionButtonText={actionText}
showProgressIndicator={showTxLoader}
title={`${actionText} ${asset.symbol}`}
onClose={close}
onAction={handleAction}

View File

@ -12,12 +12,11 @@ interface Props {
export default function UnlockModalContent(props: Props) {
const unlock = useStore((s) => s.unlock)
const showTxLoader = useStore((s) => s.showTxLoader)
const { accountId } = useParams()
async function onConfirm() {
function onConfirm() {
if (!accountId) return
await unlock({
unlock({
accountId: accountId,
vault: props.depositedVault,
amount: props.depositedVault.amounts.locked.toString(),
@ -38,7 +37,6 @@ export default function UnlockModalContent(props: Props) {
className='px-6'
rightIcon={<YesIcon />}
onClick={onConfirm}
showProgressIndicator={showTxLoader}
/>
<Button
text='No'
@ -47,7 +45,6 @@ export default function UnlockModalContent(props: Props) {
rightIcon={<NoIcon />}
tabIndex={1}
onClick={props.onClose}
disabled={showTxLoader}
/>
</div>
</>

View File

@ -40,7 +40,6 @@ export default function VaultBorrowings(props: VaultBorrowingsProps) {
const { data: prices } = usePrices()
const vaultModal = useStore((s) => s.vaultModal)
const depositIntoVault = useStore((s) => s.depositIntoVault)
const showTxLoader = useStore((s) => s.showTxLoader)
const updatedAccount = useStore((s) => s.updatedAccount)
const { computeMaxBorrowAmount } = useHealthComputer(updatedAccount)
const [percentage, setPercentage] = useState<number>(0)
@ -143,18 +142,16 @@ export default function VaultBorrowings(props: VaultBorrowingsProps) {
setPercentage(calculateSliderPercentage(maxBorrowAmounts, props.borrowings))
}
async function onConfirm() {
function onConfirm() {
if (!updatedAccount || !vaultModal) return
const isSuccess = await depositIntoVault({
depositIntoVault({
accountId: updatedAccount.id,
actions: props.depositActions,
deposits: props.deposits,
borrowings: props.borrowings,
isCreate: vaultModal.isCreate,
})
if (isSuccess) {
useStore.setState({ vaultModal: null })
}
useStore.setState({ vaultModal: null })
}
return (
@ -173,13 +170,10 @@ export default function VaultBorrowings(props: VaultBorrowingsProps) {
maxText='Max Borrow'
onChange={(amount) => updateAssets(coin.denom, amount)}
onDelete={() => onDelete(coin.denom)}
disabled={showTxLoader}
/>
)
})}
{props.borrowings.length === 1 && (
<Slider onChange={onChangeSlider} value={percentage} disabled={showTxLoader} />
)}
{props.borrowings.length === 1 && <Slider onChange={onChangeSlider} value={percentage} />}
{props.borrowings.length === 0 && (
<div className='flex items-center gap-4 py-2'>
<div className='w-4'>
@ -192,12 +186,7 @@ export default function VaultBorrowings(props: VaultBorrowingsProps) {
</div>
)}
<Button
text='Select borrow assets +'
color='tertiary'
onClick={addAsset}
disabled={showTxLoader}
/>
<Button text='Select borrow assets +' color='tertiary' onClick={addAsset} />
<DepositCapMessage
action='deposit'
@ -233,7 +222,6 @@ export default function VaultBorrowings(props: VaultBorrowingsProps) {
color='primary'
text='Deposit'
rightIcon={<ArrowRight />}
showProgressIndicator={showTxLoader}
disabled={!props.depositActions.length || props.depositCapReachedCoins.length > 0}
/>
</div>

View File

@ -18,7 +18,6 @@ import { demagnify } from 'utils/formatters'
export default function WithdrawFromVaultsModal() {
const modal = useStore((s) => s.withdrawFromVaultsModal)
const { accountId } = useParams()
const showTxLoader = useStore((s) => s.showTxLoader)
const withdrawFromVaults = useStore((s) => s.withdrawFromVaults)
const baseCurrency = useStore((s) => s.baseCurrency)
const [slippage] = useLocalStorage<number>(SLIPPAGE_KEY, DEFAULT_SETTINGS.slippage)
@ -27,9 +26,9 @@ export default function WithdrawFromVaultsModal() {
useStore.setState({ withdrawFromVaultsModal: null })
}
async function withdrawHandler() {
function withdrawHandler() {
if (!accountId || !modal) return
await withdrawFromVaults({
withdrawFromVaults({
accountId: accountId,
vaults: modal,
slippage,
@ -87,12 +86,7 @@ export default function WithdrawFromVaultsModal() {
</div>
)
})}
<Button
showProgressIndicator={showTxLoader}
onClick={withdrawHandler}
className='w-full mt-4'
text='Withdraw from all'
/>
<Button onClick={withdrawHandler} className='w-full mt-4' text='Withdraw from all' />
</div>
) : (
<CircularProgress />

View File

@ -6,8 +6,11 @@ import { ChevronDown, ChevronRight } from 'components/Icons'
import Text from 'components/Text'
import { ASSETS } from 'constants/assets'
import { BN_ZERO } from 'constants/math'
import useMarketAssets from 'hooks/useMarketAssets'
import { BNCoin } from 'types/classes/BNCoin'
import { byDenom } from 'utils/array'
import { formatValue } from 'utils/formatters'
import { convertAprToApy } from 'utils/parsers'
interface Props extends SelectOption {
isSelected?: boolean
@ -19,6 +22,7 @@ interface Props extends SelectOption {
export default function Option(props: Props) {
const isCoin = !!props.denom
const { data: marketAssets } = useMarketAssets()
function handleOnClick(value: string | undefined) {
if (!props.onClick || !value) return
@ -28,6 +32,7 @@ export default function Option(props: Props) {
if (isCoin) {
const asset = ASSETS.find((asset) => asset.denom === props.denom) ?? ASSETS[0]
const balance = props.amount ?? BN_ZERO
const marketAsset = marketAssets.find(byDenom(asset.denom))
if (props.isDisplay) {
return (
@ -74,7 +79,12 @@ export default function Option(props: Props) {
})}
</Text>
<Text size='sm' className='col-span-2 text-white/50'>
{formatValue(5, { maxDecimals: 2, minDecimals: 0, prefix: 'APY ', suffix: '%' })}
{formatValue(convertAprToApy((marketAsset?.borrowRate ?? 0) * 100, 365), {
maxDecimals: 2,
minDecimals: 0,
prefix: 'APY ',
suffix: '%',
})}
</Text>
<DisplayCurrency
className='col-span-2 text-sm text-right text-white/50'

View File

@ -104,18 +104,24 @@ export default function Select(props: Props) {
{props.title}
</Text>
)}
{props.options.map((option: SelectOption, index: number) => (
<Option
key={index}
{...option}
isSelected={
option?.value
? option?.value === selected?.value
: option?.denom === selected?.denom
}
onClick={handleChange}
/>
))}
<div className='w-full overflow-y-scroll h-70 scrollbar-hide'>
{props.options
.sort((a, b) =>
(a.amount?.toNumber() ?? 0) > (b.amount?.toNumber() ?? 0) ? -1 : 1,
)
.map((option: SelectOption, index: number) => (
<Option
key={index}
{...option}
isSelected={
option?.value
? option?.value === selected?.value
: option?.denom === selected?.denom
}
onClick={handleChange}
/>
))}
</div>
</div>
</Overlay>
</div>

View File

@ -1,127 +0,0 @@
import classNames from 'classnames'
import { ReactNode } from 'react'
import { toast as createToast, Slide, ToastContainer } from 'react-toastify'
import { mutate } from 'swr'
import { CheckCircled, Cross, CrossCircled, ExternalLink } from 'components/Icons'
import Text from 'components/Text'
import { TextLink } from 'components/TextLink'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { EXPLORER_NAME, EXPLORER_TX_URL } from 'constants/explorer'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
import useTransactionStore from 'hooks/useTransactionStore'
import useStore from 'store'
import { formatAmountWithSymbol } from 'utils/formatters'
import { BN } from 'utils/helpers'
export function generateToastContent(content: ToastSuccess['content']): ReactNode {
return content.map((item, index) => (
<div className='flex flex-wrap w-full' key={index}>
{item.coins.length > 0 && (
<>
<Text size='sm' className='w-full mb-1 text-white'>
{item.text}
</Text>
<ul className='flex flex-wrap w-full gap-1 p-1 pl-4 list-disc'>
{item.coins.map((coin) =>
BN(coin.amount).isZero() ? null : (
<li className='w-full p-0 text-sm text-white' key={coin.denom}>
{formatAmountWithSymbol(coin)}
</li>
),
)}
</ul>
</>
)}
</div>
))
}
export default function Toaster() {
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
const toast = useStore((s) => s.toast)
const isError = toast?.isError
const { addTransaction } = useTransactionStore()
if (toast) {
if (!isError) addTransaction(toast)
const Msg = () => (
<div
className={classNames(
'relative isolate m-0 flex w-full flex-wrap rounded-sm p-6 shadow-overlay backdrop-blur-lg',
'before:content-[" "] before:absolute before:inset-0 before:-z-1 before:rounded-sm before:p-[1px] before:border-glas',
isError ? 'bg-error-bg/20' : 'bg-success-bg/20',
)}
>
<div className='flex w-full gap-2 mb-4'>
<div className={classNames('rounded-sm p-1.5', isError ? 'bg-error' : 'bg-success')}>
<span className='block w-4 h-4 text-white'>
{isError ? <CrossCircled /> : <CheckCircled />}
</span>
</div>
<Text
className={classNames(
'flex items-center font-bold',
isError ? 'text-error' : 'text-success',
)}
>
{isError ? (toast.title ? toast.title : 'Error') : 'Success'}
</Text>
</div>
{!isError && toast.accountId && (
<Text className='mb-1 font-bold text-white'>{`Credit Account ${toast.accountId}`}</Text>
)}
{toast.message && (
<Text size='sm' className='w-full mb-1 text-white'>
{toast.message}
</Text>
)}
{!isError && toast.content?.length > 0 && generateToastContent(toast.content)}
{toast.hash && (
<div className='w-full'>
<TextLink
href={`${EXPLORER_TX_URL}${toast.hash}`}
target='_blank'
className={classNames(
'leading-4 underline mt-4 hover:no-underline hover:text-white',
isError ? 'text-error' : 'text-success',
)}
title={`View on ${EXPLORER_NAME}`}
>
{`View on ${EXPLORER_NAME}`}
<ExternalLink className='-mt-0.5 ml-2 inline w-3.5' />
</TextLink>
</div>
)}
<div className='absolute right-6 top-8 '>
<Cross className={classNames('h-2 w-2', isError ? 'text-error' : 'text-success')} />
</div>
</div>
)
createToast(Msg, {
icon: false,
draggable: false,
closeOnClick: true,
progressClassName: classNames('h-[1px] bg-none', isError ? 'bg-error' : 'bg-success'),
})
useStore.setState({ toast: null })
mutate(() => true)
}
return (
<ToastContainer
autoClose={5000}
closeButton={false}
position='top-right'
newestOnTop
closeOnClick
transition={reduceMotion ? undefined : Slide}
className='p-0'
toastClassName='top-[73px] z-20 m-0 mb-4 flex w-full bg-transparent p-0'
bodyClassName='p-0 m-0 w-full flex -z-1'
/>
)
}

View File

@ -0,0 +1,215 @@
import classNames from 'classnames'
import { ReactNode } from 'react'
import { Slide, ToastContainer, toast as toastify } from 'react-toastify'
import { mutate } from 'swr'
import { CheckMark } from 'components/CheckMark'
import { CircularProgress } from 'components/CircularProgress'
import { ChevronDown, Cross, CrossCircled, ExternalLink } from 'components/Icons'
import Text from 'components/Text'
import { TextLink } from 'components/TextLink'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { EXPLORER_NAME, EXPLORER_TX_URL } from 'constants/explorer'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
import useTransactionStore from 'hooks/useTransactionStore'
import useStore from 'store'
import { formatAmountWithSymbol } from 'utils/formatters'
import { BN } from 'utils/helpers'
const toastBodyClasses = classNames(
'flex flex-wrap w-full group/transaction',
'rounded-sm p-4 shadow-overlay backdrop-blur-lg',
'before:content-[" "] before:absolute before:inset-0 before:-z-1 before:rounded-sm before:p-[1px] before:border-glas',
)
function isPromise(object?: any): object is ToastPending {
if (!object) return false
return 'promise' in object
}
export function generateToastContent(content: ToastSuccess['content']): ReactNode {
return content.map((item, index) => (
<div className='flex flex-wrap w-full' key={index}>
{item.coins.length > 0 && (
<>
<Text size='sm' className='w-full mb-1 text-white'>
{item.text}
</Text>
<ul className='flex flex-wrap w-full gap-1 p-1 pl-4 list-disc'>
{item.coins.map((coin) =>
BN(coin.amount).isZero() ? null : (
<li className='w-full p-0 text-sm text-white' key={coin.denom}>
{formatAmountWithSymbol(coin)}
</li>
),
)}
</ul>
</>
)}
</div>
))
}
export default function Toaster() {
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
const toast = useStore((s) => s.toast)
const { addTransaction } = useTransactionStore()
const handlePending = (toast: ToastPending) => {
const Content = () => (
<div className='relative flex flex-wrap w-full m-0 isolate'>
<div className='flex items-center w-full gap-2 mb-2'>
<div className='rounded-sm p-1.5 pt-1 bg-info w-7 h-7 flex items-center'>
<CircularProgress size={16} />
</div>
<Text className='flex items-center font-bold text-info'>Pending Transaction</Text>
</div>
<Text size='sm' className='w-full text-white'>
Approve the transaction
<br />
and wait for its confirmation.
</Text>
</div>
)
toastify(Content, {
toastId: toast.id,
className: classNames(toastBodyClasses, 'toast-pending'),
icon: false,
draggable: false,
closeOnClick: false,
hideProgressBar: true,
autoClose: false,
})
}
const handleResponse = (toast: ToastResponse, details?: boolean) => {
const isError = toast?.isError
if (!isError) addTransaction(toast)
const generalMessage = isError ? 'Transaction failed!' : 'Transaction completed successfully!'
const showDetailElement = !!(!details && toast.hash)
const Msg = () => (
<div className='relative flex flex-wrap w-full m-0 isolate'>
<div className='flex w-full gap-2 mb-2'>
{isError ? (
<div className='rounded-sm p-1.5 bg-error'>
<span className='block w-4 h-4 text-white'>
<CrossCircled />
</span>
</div>
) : (
<div className='rounded-sm p-1.5 pt-1 bg-success w-7 h-7 flex items-center'>
<CheckMark size={16} />
</div>
)}
<Text
className={classNames(
'flex items-center font-bold',
isError ? 'text-error' : 'text-success',
)}
>
{isError ? (toast.title ? toast.title : 'Error') : 'Success'}
</Text>
</div>
<Text size='sm' className='w-full mb-2 text-white'>
{showDetailElement ? generalMessage : toast.message}
</Text>
{showDetailElement && (
<Text
size='sm'
className='flex items-center w-auto pb-0.5 text-white border-b border-white/40 border-dashed group-hover/transaction:opacity-0'
>
<ChevronDown className='w-3 mr-1' />
Transaction Details
</Text>
)}
<div
className={classNames(
'w-full flex-wrap',
showDetailElement && 'hidden group-hover/transaction:flex',
)}
>
{!isError && toast.accountId && (
<Text className='mb-1 font-bold text-white'>{`Credit Account ${toast.accountId}`}</Text>
)}
{showDetailElement && toast.message && (
<Text size='sm' className='w-full mb-1 text-white'>
{toast.message}
</Text>
)}
{!isError && toast.content?.length > 0 && generateToastContent(toast.content)}
{toast.hash && (
<div className='w-full'>
<TextLink
href={`${EXPLORER_TX_URL}${toast.hash}`}
target='_blank'
className={classNames(
'leading-4 underline mt-4 hover:no-underline hover:text-white',
isError ? 'text-error' : 'text-success',
)}
title={`View on ${EXPLORER_NAME}`}
>
{`View on ${EXPLORER_NAME}`}
<ExternalLink className='-mt-0.5 ml-2 inline w-3.5' />
</TextLink>
</div>
)}
</div>
<div className='absolute top-0 right-0'>
<Cross className={classNames('h-2 w-2', isError ? 'text-error' : 'text-success')} />
</div>
</div>
)
const toastElement = document.getElementById(String(toast.id))
if (toastElement) {
toastify.update(toast.id, {
render: Msg,
className: toastBodyClasses,
type: isError ? 'error' : 'success',
icon: false,
draggable: false,
closeOnClick: true,
autoClose: 5000,
progressClassName: classNames('h-[1px] bg-none', isError ? 'bg-error' : 'bg-success'),
hideProgressBar: false,
})
} else {
toastify(Msg, {
toastId: toast.id,
className: toastBodyClasses,
type: isError ? 'error' : 'success',
icon: false,
draggable: false,
closeOnClick: true,
autoClose: 5000,
progressClassName: classNames('h-[1px] bg-none', isError ? 'bg-error' : 'bg-success'),
})
}
useStore.setState({ toast: null })
mutate(() => true)
}
if (toast) {
if (isPromise(toast)) {
handlePending(toast)
} else {
handleResponse(toast)
}
}
return (
<ToastContainer
closeButton={false}
position='top-right'
newestOnTop
closeOnClick={false}
transition={reduceMotion ? undefined : Slide}
bodyClassName='p-0 m-0 -z-1'
className='mt-[73px]'
/>
)
}

View File

@ -1,307 +0,0 @@
import classNames from 'classnames'
import Text from 'components/Text'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
export default function TransactionLoader() {
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
return (
<>
<div className='absolute z-50 flex flex-wrap items-center content-center justify-center w-full h-full text-white bg-black/80'>
<div className='w-[120px] h-[120px]'>
<svg version='1.1' x='0px' y='0px' viewBox='0 0 120 120'>
<path
className={classNames(
!reduceMotion && 'opacity-0 animate-loaderFade',
'animate-delay-[500ms]',
)}
fill='none'
stroke='currentColor'
strokeWidth='1'
strokeMiterlimit='10'
d='M104.5,27.7L92.3,15.5L77,7.7L60,5
L43,7.7l-15.3,7.8L15.5,27.7L7.7,43L5,60l2.7,17l7.8,15.3l12.2,12.2l15.3,7.8l17,2.7l17-2.7l15.3-7.8l12.2-12.2l7.8-15.3l2.7-17
l-2.7-17L104.5,27.7z'
/>
<line
className={classNames(
!reduceMotion && 'opacity-0 animate-loaderFade',
'animate-delay-[700ms]',
)}
fill='none'
stroke='currentColor'
strokeMiterlimit='10'
x1='60'
y1='60'
x2='27.7'
y2='15.5'
/>
<line
className={classNames(
!reduceMotion && 'opacity-0 animate-loaderFade',
'animate-delay-[900ms]',
)}
fill='none'
stroke='currentColor'
strokeMiterlimit='10'
x1='60'
y1='60'
x2='92.3'
y2='15.5'
/>
<line
className={classNames(
!reduceMotion && 'opacity-0 animate-loaderFade',
'animate-delay-[1100ms]',
)}
fill='none'
stroke='currentColor'
strokeMiterlimit='10'
x1='112.3'
y1='77'
x2='60'
y2='60'
/>
<line
className={classNames(
!reduceMotion && 'opacity-0 animate-loaderFade',
'animate-delay-[1300ms]',
)}
fill='none'
stroke='currentColor'
strokeMiterlimit='10'
x1='60'
y1='115'
x2='60'
y2='60'
/>
<line
className={classNames(
!reduceMotion && 'opacity-0 animate-loaderFade',
'animate-delay-[1500ms]',
)}
fill='none'
stroke='currentColor'
strokeMiterlimit='10'
x1='7.7'
y1='77'
x2='60'
y2='60'
/>
<polygon
className={classNames(
!reduceMotion && 'opacity-0 animate-loaderFade',
'animate-delay-[1800ms]',
)}
fill='none'
stroke='currentColor'
strokeMiterlimit='10'
points='47.5,42.9 72.3,43 80.1,66.5 60,81.1
39.8,66.5 '
/>
<polygon
className={classNames(
!reduceMotion && 'opacity-0 animate-loaderFade',
'animate-delay-[2100ms]',
)}
fill='none'
stroke='currentColor'
strokeMiterlimit='10'
points='60,22.1 47.5,42.9 23.9,48.3 39.8,66.5
37.7,90.7 60,81.1 82.3,90.7 80.1,66.5 96.1,48.3 72.3,43 '
/>
<polygon
className={classNames(
!reduceMotion && 'opacity-0 animate-loaderFade',
'animate-delay-[2400ms]',
)}
fill='none'
stroke='currentColor'
strokeMiterlimit='10'
points='34.4,24.8 60,22.1 85.5,24.8 96.1,48.3
101.3,73.4 82.3,90.7 60,103.5 37.7,90.7 18.6,73.4 23.9,48.3 '
/>
<polygon
className={classNames(
!reduceMotion && 'opacity-0 animate-loaderFade',
'animate-delay-[2500ms]',
)}
fill='none'
stroke='currentColor'
strokeLinejoin='round'
strokeMiterlimit='10'
points='34.4,24.8 43,7.7
60,22.1 77,7.7 85.5,24.8 104.5,27.7 96.1,48.3 115,60 101.3,73.4 104.5,92.3 82.3,90.7 77,112.3 60,103.5 43,112.3 37.7,90.7
15.5,92.3 18.6,73.4 5,60 23.9,48.3 15.5,27.7 '
/>
<line
className={classNames(
!reduceMotion && 'opacity-0 animate-loaderFade',
'animate-delay-[2500ms]',
)}
fill='none'
stroke='currentColor'
strokeMiterlimit='10'
x1='60'
y1='5'
x2='60'
y2='22.1'
/>
<line
className={classNames(
!reduceMotion && 'opacity-0 animate-loaderFade',
'animate-delay-[2500ms]',
)}
fill='none'
stroke='currentColor'
strokeMiterlimit='10'
x1='112.3'
y1='43'
x2='96.1'
y2='48.3'
/>
<line
className={classNames(
!reduceMotion && 'opacity-0 animate-loaderFade',
'animate-delay-[2500ms]',
)}
fill='none'
stroke='currentColor'
strokeMiterlimit='10'
x1='92.3'
y1='104.5'
x2='82.3'
y2='90.7'
/>
<line
className={classNames(
!reduceMotion && 'opacity-0 animate-loaderFade',
'animate-delay-[2500ms]',
)}
fill='none'
stroke='currentColor'
strokeMiterlimit='10'
x1='27.7'
y1='104.5'
x2='37.7'
y2='90.7'
/>
<line
className={classNames(
!reduceMotion && 'opacity-0 animate-loaderFade',
'animate-delay-[2500ms]',
)}
fill='none'
stroke='currentColor'
strokeLinejoin='round'
strokeMiterlimit='10'
x1='7.7'
y1='43'
x2='23.9'
y2='48.3'
/>
<polygon
className={classNames(
!reduceMotion && 'animate-loaderGlow',
'opacity-0 animate-delay-[5500ms]',
)}
fill='currentColor'
points='60,60 72.3,43 80.1,66.5 '
/>
<polygon
className={classNames(
!reduceMotion && 'animate-loaderGlow',
'opacity-0 animate-delay-[11000ms]',
)}
fill='currentColor'
points='60,22.1 47.5,42.9 34.4,24.8 '
/>
<polygon
className={classNames(
!reduceMotion && 'animate-loaderGlow',
'opacity-0 animate-delay-[16500ms]',
)}
fill='currentColor'
points='39.8,66.5 37.7,90.7 60,81.1 '
/>
<polygon
className={classNames(
!reduceMotion && 'animate-loaderGlow',
'opacity-0 animate-delay-[22000ms]',
)}
fill='currentColor'
points='18.6,73.4 23.9,48.3 5,60 '
/>
<polygon
className={classNames(
!reduceMotion && 'animate-loaderGlow',
'opacity-0 animate-delay-[27500ms]',
)}
fill='currentColor'
points='82.3,90.7 101.3,73.4 104.5,92.3 '
/>
<polygon
className={classNames(
!reduceMotion && 'animate-loaderGlow',
'opacity-0 animate-delay-[33000ms]',
)}
fill='currentColor'
points='96.1,48.3 104.5,27.7 112.3,43 '
/>
<polygon
className={classNames(
!reduceMotion && 'animate-loaderGlow',
'opacity-0 animate-delay-[38500ms]',
)}
fill='currentColor'
points='85.5,24.8 77,7.7 91.9,15.3 '
/>
<polygon
className={classNames(
!reduceMotion && 'animate-loaderGlow',
'opacity-0 animate-delay-[44000ms]',
)}
fill='currentColor'
points='60,103.5 82.3,90.7 60,81.1 '
/>
<polygon
className={classNames(
!reduceMotion && 'animate-loaderGlow',
'opacity-0 animate-delay-[49500ms]',
)}
fill='currentColor'
points='15.8,92.3 37.7,90.7 18.6,73.4 '
/>
<polygon
className={classNames(
!reduceMotion && 'animate-loaderGlow',
'opacity-0 animate-delay-[55000ms]',
)}
fill='currentColor'
points='15.8,27.4 34.4,24.8 23.9,48.3 '
/>
<polygon
className={classNames(
!reduceMotion && 'animate-loaderGlow',
'opacity-0 animate-delay-[60500ms]',
)}
fill='currentColor'
points='43,7.7 60,5 60,22.1 '
/>
</svg>
</div>
<Text
className={classNames(
'p-4 text-center text-white/70 w-full',
!reduceMotion && 'animate-fadein delay-4000',
)}
>
Broadcasting transaction...
</Text>
</div>
</>
)
}

View File

@ -2,6 +2,7 @@ import { useShuttle } from '@delphi-labs/shuttle-react'
import Image from 'next/image'
import React, { useEffect, useState } from 'react'
import QRCode from 'react-qr-code'
import moment from 'moment'
import Button from 'components/Button'
import FullOverlayContent from 'components/FullOverlayContent'
@ -104,6 +105,7 @@ export default function WalletSelect(props: Props) {
if (error?.message && error?.title) {
useStore.setState({
toast: {
id: moment.now(),
isError: true,
title: error.title,
message: error.message,

View File

@ -8,12 +8,12 @@ import Footer from 'components/Footer'
import DesktopHeader from 'components/Header/DesktopHeader'
import ModalsContainer from 'components/Modals/ModalsContainer'
import PageMetadata from 'components/PageMetadata'
import Toaster from 'components/Toaster'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useCurrentAccount from 'hooks/useCurrentAccount'
import useLocalStorage from 'hooks/useLocalStorage'
import useStore from 'store'
import Toaster from 'components/Toaster'
interface Props {
focusComponent: FocusComponent | null

View File

@ -41,11 +41,13 @@ export default function createBroadcastSlice(
get: GetState<Store>,
): BroadcastSlice {
const handleResponseMessages = (props: HandleResponseProps) => {
const { accountId, response, action, lend, changes, target, message } = props
const { id, accountId, response, action, lend, changes, target, message } = props
if (!response) return
if (response.error || response.result?.response.code !== 0) {
set({
toast: {
id,
message: generateErrorMessage(response),
isError: true,
hash: response.result?.hash,
@ -55,6 +57,7 @@ export default function createBroadcastSlice(
}
const toast: ToastResponse = {
id,
accountId: accountId,
isError: false,
hash: response?.result?.hash,
@ -152,7 +155,6 @@ export default function createBroadcastSlice(
return {
toast: null,
showTxLoader: false,
borrow: async (options: { accountId: string; coin: BNCoin; borrowToWallet: boolean }) => {
const borrowAction: Action = { borrow: options.coin.toCoin() }
const withdrawAction: Action = { withdraw: options.coin.toActionCoin() }
@ -174,48 +176,45 @@ export default function createBroadcastSlice(
lend: { denom: options.coin.denom, amount: 'account_balance' },
})
}
const response = await get().executeMsg({
const response = get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
handleResponseMessages({
get().setToast({
response,
action: 'borrow',
lend: checkAutoLendEnabled(options.accountId),
target: options.borrowToWallet ? 'wallet' : 'account',
accountId: options.accountId,
changes: { debts: [options.coin] },
options: {
action: 'borrow',
lend: checkAutoLendEnabled(options.accountId),
target: options.borrowToWallet ? 'wallet' : 'account',
accountId: options.accountId,
changes: { debts: [options.coin] },
},
})
return !!response.result
return response.then((response) => !!response.result)
},
createAccount: async () => {
const msg: CreditManagerExecuteMsg = {
create_credit_account: 'default',
}
const response = await get().executeMsg({
const response = get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
set({ createAccountModal: false })
const id = response.result
? getSingleValueFromBroadcastResult(response.result, 'wasm', 'token_id')
: null
handleResponseMessages({
get().setToast({
response,
action: 'create',
accountId: id ?? undefined,
message: id ? `Created the Credit Account` : undefined,
options: {
action: 'create',
message: `Created the Credit Account`,
},
})
if (id)
set({
fundAccountModal: true,
})
return id
return response.then((response) =>
response.result
? getSingleValueFromBroadcastResult(response.result, 'wasm', 'token_id')
: null,
)
},
deleteAccount: async (options: { accountId: string; lends: BNCoin[] }) => {
const reclaimMsg = options.lends.map((coin) => {
@ -237,21 +236,23 @@ export default function createBroadcastSlice(
},
}
const response = await get().executeMsg({
const response = get().executeMsg({
messages: [
generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, refundMessage, []),
generateExecutionMessage(get().address, ENV.ADDRESS_ACCOUNT_NFT, burnMessage, []),
],
})
handleResponseMessages({
get().setToast({
response,
action: 'delete',
accountId: options.accountId,
message: `Deleted the Credit Account`,
options: {
action: 'delete',
accountId: options.accountId,
message: `Deleted the Credit Account`,
},
})
return !!response.result
return response.then((response) => !!response.result)
},
claimRewards: (options: { accountId: string }) => {
const msg: CreditManagerExecuteMsg = {
@ -271,18 +272,20 @@ export default function createBroadcastSlice(
const estimateFee = () => getEstimatedFee(messages)
const execute = async () => {
const response = await get().executeMsg({
const response = get().executeMsg({
messages,
})
handleResponseMessages({
get().setToast({
response,
action: 'create',
accountId: options.accountId,
message: `Claimed rewards`,
options: {
action: 'claim',
accountId: options.accountId,
message: `Claimed rewards`,
},
})
return !!response.result
return response.then((response) => !!response.result)
}
return { estimateFee, execute }
@ -307,19 +310,21 @@ export default function createBroadcastSlice(
const funds = options.coins.map((coin) => coin.toCoin())
const response = await get().executeMsg({
const response = get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, funds)],
})
handleResponseMessages({
get().setToast({
response,
action: 'deposit',
lend: options.lend,
accountId: options.accountId,
changes: { deposits: options.coins },
options: {
action: 'deposit',
lend: options.lend,
accountId: options.accountId,
changes: { deposits: options.coins },
},
})
return !!response.result
return response.then((response) => !!response.result)
},
unlock: async (options: { accountId: string; vault: DepositedVault; amount: string }) => {
const msg: CreditManagerExecuteMsg = {
@ -336,17 +341,20 @@ export default function createBroadcastSlice(
},
}
const response = await get().executeMsg({
const response = get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
handleResponseMessages({
get().setToast({
response,
action: 'unlock',
accountId: options.accountId,
message: `Requested unlock for ${options.vault.name}`,
options: {
action: 'unlock',
accountId: options.accountId,
message: `Requested unlock for ${options.vault.name}`,
},
})
return !!response.result
return response.then((response) => !!response.result)
},
withdrawFromVaults: async (options: {
@ -391,18 +399,22 @@ export default function createBroadcastSlice(
}
}
const response = await get().executeMsg({
const response = get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
const vaultsString = options.vaults.length === 1 ? 'vault' : 'vaults'
handleResponseMessages({
get().setToast({
response,
action: 'withdraw',
accountId: options.accountId,
message: `Withdrew ${options.vaults.length} unlocked ${vaultsString} to the account`,
options: {
action: 'withdraw',
accountId: options.accountId,
message: `Withdrew ${options.vaults.length} unlocked ${vaultsString} to the account`,
},
})
return !!response.result
return response.then((response) => !!response.result)
},
depositIntoVault: async (options: {
accountId: string
@ -418,20 +430,22 @@ export default function createBroadcastSlice(
},
}
const response = await get().executeMsg({
const response = get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
const depositedCoins = getVaultDepositCoinsFromActions(options.actions)
handleResponseMessages({
get().setToast({
response,
action: options.isCreate ? 'vaultCreate' : 'vault',
accountId: options.accountId,
changes: { deposits: depositedCoins },
options: {
action: options.isCreate ? 'vaultCreate' : 'vault',
accountId: options.accountId,
changes: { deposits: depositedCoins },
},
})
return !!response.result
return response.then((response) => !!response.result)
},
withdraw: async (options: {
accountId: string
@ -456,19 +470,21 @@ export default function createBroadcastSlice(
},
}
const response = await get().executeMsg({
const response = get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
handleResponseMessages({
get().setToast({
response,
action: 'withdraw',
target: 'wallet',
accountId: options.accountId,
changes: { deposits: options.coins.map((coin) => coin.coin) },
options: {
action: 'withdraw',
target: 'wallet',
accountId: options.accountId,
changes: { deposits: options.coins.map((coin) => coin.coin) },
},
})
return !!response.result
return response.then((response) => !!response.result)
},
repay: async (options: {
accountId: string
@ -494,18 +510,20 @@ export default function createBroadcastSlice(
},
}
const response = await get().executeMsg({
const response = get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
handleResponseMessages({
get().setToast({
response,
action: 'repay',
accountId: options.accountId,
changes: { deposits: [options.coin] },
options: {
action: 'repay',
accountId: options.accountId,
changes: { deposits: [options.coin] },
},
})
return !!response.result
return response.then((response) => !!response.result)
},
lend: async (options: { accountId: string; coin: BNCoin; isMax?: boolean }) => {
const msg: CreditManagerExecuteMsg = {
@ -519,18 +537,20 @@ export default function createBroadcastSlice(
},
}
const response = await get().executeMsg({
const response = get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
handleResponseMessages({
get().setToast({
response,
action: 'lend',
accountId: options.accountId,
changes: { lends: [options.coin] },
options: {
action: 'lend',
accountId: options.accountId,
changes: { lends: [options.coin] },
},
})
return !!response.result
return response.then((response) => !!response.result)
},
reclaim: async (options: { accountId: string; coin: BNCoin; isMax?: boolean }) => {
const msg: CreditManagerExecuteMsg = {
@ -544,19 +564,21 @@ export default function createBroadcastSlice(
},
}
const response = await get().executeMsg({
const response = get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
handleResponseMessages({
get().setToast({
response,
action: 'withdraw',
target: 'account',
accountId: options.accountId,
changes: { deposits: [options.coin] },
options: {
action: 'withdraw',
target: 'account',
accountId: options.accountId,
changes: { deposits: [options.coin] },
},
})
return !!response.result
return response.then((response) => !!response.result)
},
swap: (options: {
accountId: string
@ -600,31 +622,60 @@ export default function createBroadcastSlice(
const estimateFee = () => getEstimatedFee(messages)
const execute = async () => {
const response = await get().executeMsg({
const response = get().executeMsg({
messages,
})
const coinOut = getTokenOutFromSwapResponse(response, options.denomOut)
const successMessage = `Swapped ${formatAmountWithSymbol(
options.coinIn.toCoin(),
)} for ${formatAmountWithSymbol(coinOut)}`
const swapOptions = { denomOut: options.denomOut, coinIn: options.coinIn }
handleResponseMessages({
get().setToast({
response,
action: 'swap',
message: successMessage,
accountId: options.accountId,
options: {
action: 'swap',
accountId: options.accountId,
},
swapOptions,
})
return !!response.result
return response.then((response) => !!response.result)
}
return { estimateFee, execute }
},
setToast: (toast: ToastObject) => {
const id = moment().unix()
set({
toast: {
id,
promise: toast.response,
},
})
toast.response.then((response) => {
if (toast.options.action === 'create') {
toast.options.accountId =
getSingleValueFromBroadcastResult(response.result, 'wasm', 'token_id') ?? undefined
}
if (toast.options.action === 'swap' && toast.swapOptions) {
const coinOut = getTokenOutFromSwapResponse(response, toast.swapOptions.denomOut)
const successMessage = `Swapped ${formatAmountWithSymbol(
toast.swapOptions.coinIn.toCoin(),
)} for ${formatAmountWithSymbol(coinOut)}`
toast.options.message = successMessage
}
handleResponseMessages({
id,
response,
...toast.options,
})
})
},
executeMsg: async (options: { messages: MsgExecuteContract[] }): Promise<BroadcastResult> => {
try {
const client = get().client
if (!client) return { error: 'no client detected' }
set({ showTxLoader: true })
const fee = await getEstimatedFee(options.messages)
const broadcastOptions = {
messages: options.messages,
@ -634,9 +685,7 @@ export default function createBroadcastSlice(
wallet: client.connectedWallet,
mobile: isMobile,
}
const result = await client.broadcast(broadcastOptions)
set({ showTxLoader: false })
if (result.hash) {
return { result }
}

View File

@ -6,8 +6,6 @@ export default function createModalSlice(set: SetState<ModalSlice>, get: GetStat
accountDeleteModal: null,
alertDialog: null,
borrowModal: null,
createAccountModal: false,
fundAccountModal: false,
fundAndWithdrawModal: null,
getStartedModal: false,
lendAndReclaimModal: null,

View File

@ -42,3 +42,18 @@
appearance: none;
}
}
.Toastify__toast {
transition-property: background-color;
transition-duration: 1500ms;
transition-timing-function: ease-in-out;
background-color: #fedb7c33;
}
.Toastify__toast--success {
background: #6ce9a633;
}
.Toastify__toast--error {
background-color: #fda29b33 !important;
}

View File

@ -11,7 +11,26 @@ interface ExecutableTx {
estimateFee: () => Promise<StdFee>
}
interface ToastObjectOptions extends HandleResponseProps {
id?: number
}
interface ToastObject {
response: Promise<BroadcastResult>
options: ToastObjectOptions
swapOptions?: {
coinIn: BNCoin
denomOut: string
}
}
interface ToastPending {
id: number
promise: Promise<BroadcastResult>
}
type ToastResponse = {
id: number
hash?: string
title?: string
} & (ToastSuccess | ToastError)
@ -36,7 +55,8 @@ interface ToastStore {
}
interface HandleResponseProps {
response: BroadcastResult
id: number
response?: BroadcastResult
action:
| 'deposit'
| 'withdraw'
@ -83,6 +103,7 @@ interface BroadcastSlice {
accountBalance?: boolean
lend?: BNCoin
}) => Promise<boolean>
setToast: (toast: ToastObject) => void
swap: (options: {
accountId: string
coinIn: BNCoin
@ -92,7 +113,7 @@ interface BroadcastSlice {
slippage: number
isMax?: boolean
}) => ExecutableTx
toast: ToastResponse | null
toast: ToastResponse | ToastPending | null
unlock: (options: {
accountId: string
vault: DepositedVault
@ -109,5 +130,4 @@ interface BroadcastSlice {
borrow: BNCoin[]
reclaims: ActionCoin[]
}) => Promise<boolean>
showTxLoader: boolean
}

View File

@ -11,6 +11,6 @@ interface CommonSlice {
}
interface FocusComponent {
component: ReactNode
component: import('react').JSX.Element | null
onClose?: () => void
}

View File

@ -3,8 +3,6 @@ interface ModalSlice {
addVaultBorrowingsModal: AddVaultBorrowingsModal | null
alertDialog: AlertDialogConfig | null
borrowModal: BorrowModal | null
createAccountModal: boolean
fundAccountModal: boolean
fundAndWithdrawModal: 'fund' | 'withdraw' | null
getStartedModal: boolean
lendAndReclaimModal: LendAndReclaimModalConfig | null

View File

@ -39,6 +39,8 @@ module.exports = {
theme: {
extend: {
animation: {
check: 'check 1.5s ease-in-out forwards',
circle: 'circle 1.5s ease-in-out forwards',
fadein: 'fadein 1s ease-in-out forwards',
glow: 'glow 1000ms ease-in-out forwards',
progress: 'spin 1200ms cubic-bezier(0.5, 0, 0.5, 1) infinite',
@ -139,11 +141,28 @@ module.exports = {
4.5: '18px',
15: '60px',
55: '220px',
70: '280px',
},
hueRotate: {
'-82': '-82deg',
},
keyframes: {
check: {
'0%': {
strokeDashoffset: -100,
},
'100%': {
strokeDashoffset: 900,
},
},
circle: {
'0%': {
strokeDashoffset: 1000,
},
'100%': {
strokeDashoffset: 0,
},
},
fadein: {
'0%': { opacity: 0 },
'100%': { opacity: 1 },
@ -178,6 +197,7 @@ module.exports = {
8: '32px',
10: '40px',
14: '56px',
30.5: '122px',
},
maxWidth: {
content: '1024px',