diff --git a/src/components/Toast/Toast.styled.ts b/src/components/Toast/Toast.styled.ts index f876b172a..6fb46abde 100644 --- a/src/components/Toast/Toast.styled.ts +++ b/src/components/Toast/Toast.styled.ts @@ -70,6 +70,12 @@ export const STitle = styled(Title)` & .referralDesc { color: ${theme.colors.white}; } + + & strong, + & b { + font-weight: 700; + font-family: "ChakraPetchBold"; + } ` export const SClose = styled(Close)` diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx index 575619330..1482ea3a0 100644 --- a/src/components/Toast/Toast.tsx +++ b/src/components/Toast/Toast.tsx @@ -17,6 +17,7 @@ import { ToastVariant } from "state/toasts" type Props = { variant?: ToastVariant title?: string | ReactNode + description?: string | ReactNode link?: string actions?: ReactNode index?: number @@ -31,6 +32,7 @@ type Props = { export const Toast: FC = ({ variant = "info", title, + description, link, actions, index, @@ -56,6 +58,7 @@ export const Toast: FC = ({ title?: string | ReactNode + description?: string | ReactNode link?: string actions?: ReactNode meta?: ReactNode @@ -45,13 +46,24 @@ export function ToastContent(props: {
- - {typeof props.title === "string" ? ( -

- ) : ( - props.title +

+ + {typeof props.title === "string" ? ( +

+ ) : ( + props.title + )} + + {props.description && ( + + {typeof props.description === "string" ? ( +

+ ) : ( + props.description + )} + )} - +

{props.actions}
diff --git a/src/components/Toast/sidebar/ToastSidebar.tsx b/src/components/Toast/sidebar/ToastSidebar.tsx index c9d5d7a28..0a9e9112b 100644 --- a/src/components/Toast/sidebar/ToastSidebar.tsx +++ b/src/components/Toast/sidebar/ToastSidebar.tsx @@ -90,6 +90,15 @@ export function ToastSidebar() { }} /> } + description={ + toast.description ? ( +
+ ) : undefined + } actions={toast.actions} dateCreated={ typeof toast.dateCreated === "string" @@ -116,6 +125,15 @@ export function ToastSidebar() { }} /> } + description={ + toast.description ? ( +
+ ) : undefined + } actions={toast.actions} dateCreated={ typeof toast.dateCreated === "string" diff --git a/src/sections/trade/sections/otc/modals/PlaceOrder.tsx b/src/sections/trade/sections/otc/modals/PlaceOrder.tsx index 3d4f902e1..2507d9224 100644 --- a/src/sections/trade/sections/otc/modals/PlaceOrder.tsx +++ b/src/sections/trade/sections/otc/modals/PlaceOrder.tsx @@ -13,12 +13,13 @@ import { AssetsModalContent } from "sections/assets/AssetsModal" import { getFixedPointAmount } from "utils/balance" import { BN_10 } from "utils/constants" import { FormValues } from "utils/helpers" -import { useStore } from "state/store" +import { ToastMessage, useStore } from "state/store" import { OrderAssetSelect } from "./cmp/AssetSelect" import { OrderAssetRate } from "./cmp/AssetXRate" import { PartialOrderToggle } from "./cmp/PartialOrderToggle" import { useRpcProvider } from "providers/rpcProvider" import { useAccount } from "sections/web3-connect/Web3Connect.utils" +import { TOAST_MESSAGES } from "state/toasts" type PlaceOrderProps = { assetOut?: u32 | string @@ -109,6 +110,24 @@ export const PlaceOrder = ({ assetInMeta.decimals, ).decimalPlaces(0, 1) + const toast = TOAST_MESSAGES.reduce((memo, type) => { + const msType = type === "onError" ? "onLoading" : type + memo[type] = ( + + + + + ) + return memo + }, {} as ToastMessage) + await createTransaction( { tx: api.tx.otc.placeOrder( @@ -125,34 +144,7 @@ export const PlaceOrder = ({ onClose() form.reset() }, - toast: { - onLoading: ( - - - - - ), - onSuccess: ( - - - - - ), - }, + toast, }, ) } diff --git a/src/sections/transaction/ReviewTransaction.tsx b/src/sections/transaction/ReviewTransaction.tsx index a9074be6c..07d1e0145 100644 --- a/src/sections/transaction/ReviewTransaction.tsx +++ b/src/sections/transaction/ReviewTransaction.tsx @@ -30,7 +30,7 @@ export const ReviewTransaction = (props: Transaction) => { txState, reset, txLink, - } = useSendTx() + } = useSendTx({ id: props.id }) const isError = isSendError || !!signError const error = sendError || signError diff --git a/src/sections/transaction/ReviewTransaction.utils.tsx b/src/sections/transaction/ReviewTransaction.utils.tsx index e76f5ba0d..4fe2eb262 100644 --- a/src/sections/transaction/ReviewTransaction.utils.tsx +++ b/src/sections/transaction/ReviewTransaction.utils.tsx @@ -18,6 +18,7 @@ import { useEvmAccount } from "sections/web3-connect/Web3Connect.utils" import { PermitResult } from "sections/web3-connect/signer/EthereumSigner" import { useToast } from "state/toasts" import { H160, getEvmChainById, getEvmTxLink, isEvmAccount } from "utils/evm" +import { defer } from "utils/helpers" import { getSubscanLinkByType } from "utils/formatting" type TxMethod = AnyJson & { @@ -87,6 +88,14 @@ export function getTransactionJSON(tx: SubmittableExtrinsic<"promise">) { export class UnknownTransactionState extends Error {} +export class TransactionError extends Error { + docs: string = "" + method: string = "" + constructor(public error: string) { + super(error) + } +} + function evmTxReceiptToSubmittableResult(txReceipt: TransactionReceipt) { const isSuccess = txReceipt.status === 1 const submittableResult: ISubmittableResult = { @@ -123,18 +132,24 @@ const createResultOnCompleteHandler = (result: ISubmittableResult) => { if (result.isCompleted) { if (result.dispatchError) { + let docs = "" + let method = "" let errorMessage = result.dispatchError.toString() if (result.dispatchError.isModule) { const decoded = api.registry.findMetaError( result.dispatchError.asModule, ) - errorMessage = `${decoded.section}.${ - decoded.method - }: ${decoded.docs.join(" ")}` + docs = decoded.docs.join(" ") + method = decoded.method + errorMessage = `${decoded.section}.${decoded.method}: ${docs}` } - onError(new Error(errorMessage)) + const error = new TransactionError(errorMessage) + error.docs = docs + error.method = method + + onError(error) } else { onSuccess(result) } @@ -145,8 +160,10 @@ const createResultOnCompleteHandler = export const useSendEvmTransactionMutation = ( options: MutationObserverOptions< - ISubmittableResult, - unknown, + ISubmittableResult & { + transactionLink?: string + }, + TransactionError, { evmTx: TransactionResponse tx?: SubmittableExtrinsic<"promise"> @@ -177,7 +194,7 @@ export const useSendEvmTransactionMutation = ( return resolve(evmTxReceiptToSubmittableResult(receipt)) } catch (err) { const { error } = decodeError(err) - reject(new Error(error)) + reject(new TransactionError(error)) } finally { clearTimeout(timeout) } @@ -279,8 +296,8 @@ export const useSendDispatchPermit = ( export const useSendTransactionMutation = ( options: MutationObserverOptions< - ISubmittableResult, - unknown, + ISubmittableResult & { transactionLink?: string }, + TransactionError, SubmittableExtrinsic<"promise"> > = {}, ) => { @@ -408,12 +425,32 @@ const useBoundReferralToast = () => { } } -export const useSendTx = () => { +const useErrorToastUpdate = (id: string) => { + const { edit } = useToast() + + return (err: TransactionError) => { + if (err?.method) { + defer(() => { + edit(id, { + description: ( +

+ {err.method} + {err.docs ? ` - ${err.docs}` : ""} +

+ ), + }) + }) + } + } +} + +export const useSendTx = ({ id }: { id: string }) => { const [txType, setTxType] = useState<"default" | "evm" | "permit" | null>( null, ) const boundReferralToast = useBoundReferralToast() + const updateErrorToast = useErrorToastUpdate(id) const sendTx = useSendTransactionMutation({ onMutate: (tx) => { @@ -421,6 +458,7 @@ export const useSendTx = () => { setTxType("default") }, onSuccess: boundReferralToast.onSuccess, + onError: updateErrorToast, }) const sendEvmTx = useSendEvmTransactionMutation({ @@ -429,6 +467,7 @@ export const useSendTx = () => { setTxType("evm") }, onSuccess: boundReferralToast.onSuccess, + onError: updateErrorToast, }) const sendPermitTx = useSendDispatchPermit({ diff --git a/src/sections/transaction/ReviewTransactionToast.tsx b/src/sections/transaction/ReviewTransactionToast.tsx index 97fc15115..ae72262f5 100644 --- a/src/sections/transaction/ReviewTransactionToast.tsx +++ b/src/sections/transaction/ReviewTransactionToast.tsx @@ -34,6 +34,7 @@ export function ReviewTransactionToast(props: { if (isSuccess) { // toast should be still present, even if ReviewTransaction is unmounted toastRef.current.success({ + id: props.id, title: props.toastMessage?.onSuccess ?? (

{t("liquidity.reviewTransaction.toast.success")}

), @@ -48,6 +49,7 @@ export function ReviewTransactionToast(props: { if (isError) { if (error instanceof UnknownTransactionState) { toastRef.current.unknown({ + id: props.id, link: props.link, title: props.toastMessage?.onError ?? (

{t("liquidity.reviewTransaction.toast.unknown")}

@@ -55,6 +57,7 @@ export function ReviewTransactionToast(props: { }) } else { toastRef.current.error({ + id: props.id, link: props.link, title: props.toastMessage?.onError ?? (

{t("liquidity.reviewTransaction.toast.error")}

@@ -65,6 +68,7 @@ export function ReviewTransactionToast(props: { if (isLoading) { toRemoveId = toastRef.current.loading({ + id: props.id, link: props.link, title: props.toastMessage?.onLoading ?? (

{t("liquidity.reviewTransaction.toast.pending")}

@@ -75,7 +79,16 @@ export function ReviewTransactionToast(props: { return () => { if (toRemoveId) toastRef.current.remove(toRemoveId) } - }, [t, props.toastMessage, isError, error, isSuccess, isLoading, props.link]) + }, [ + t, + props.toastMessage, + isError, + error, + isSuccess, + isLoading, + props.link, + props.id, + ]) return null } diff --git a/src/state/toasts.ts b/src/state/toasts.ts index e9eeb62b5..c54941158 100644 --- a/src/state/toasts.ts +++ b/src/state/toasts.ts @@ -21,6 +21,7 @@ type ToastParams = { id?: string link?: string title: ReactElement + description?: ReactElement actions?: ReactNode persist?: boolean hideTime?: number @@ -32,6 +33,7 @@ type ToastData = ToastParams & { hidden: boolean dateCreated: string title: string + description?: string } type PersistState = { @@ -48,7 +50,6 @@ interface ToastStore { accoutAddress: Maybe, callback: (toasts: Array) => Array, ) => void - sidebar: boolean setSidebar: (value: boolean) => void updateToastsTemp: ( @@ -292,6 +293,26 @@ export const useToast = () => { return id } + const edit = (id: string, toast: Partial) => { + const title = toast.title ? renderToString(toast.title) : undefined + const description = toast.description + ? renderToString(toast.description) + : undefined + + store.update(account?.address, (toasts) => + toasts.map((t) => + t.id === id + ? ({ + ...t, + ...toast, + title: title ?? t.title, + description: description ?? t.description, + } as ToastData) + : t, + ), + ) + } + const info = (toast: ToastParams) => add("info", toast) const success = (toast: ToastParams) => add("success", toast) const error = (toast: ToastParams) => add("error", toast) @@ -331,6 +352,7 @@ export const useToast = () => { toastsTemp: store.toastsTemp, setSidebar, add, + edit, hide, remove, info, diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index f2b1d0be9..3bb334fca 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -321,3 +321,15 @@ export function abbreviateNumber(price: BN): string { return formattedPrice } + +export function defer(callback: () => void) { + if (typeof callback === "function") { + if ("requestIdleCallback" in window) { + const id = requestIdleCallback(callback) + return () => cancelIdleCallback(id) + } else { + const id = setTimeout(callback, 1) + return () => clearTimeout(id) + } + } +}