Skip to content

Commit 3c41a98

Browse files
committed
SD-69: Show pending transaction toast
1 parent 306be56 commit 3c41a98

File tree

4 files changed

+84
-17
lines changed

4 files changed

+84
-17
lines changed

src/domains/misc/components/CIcon/icons/spinner.svg

Lines changed: 4 additions & 4 deletions
Loading

src/domains/misc/components/Toast/Toast.tsx

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useDocumentVisibility } from '@react-hookz/web';
22
import dayjs from 'dayjs';
33
import { ComponentProps, MouseEvent, MouseEventHandler, PointerEvent, ReactNode } from 'react';
4-
import styled, { css } from 'styled-components';
4+
import styled from 'styled-components';
55

66
import Button from 'src/domains/misc/components/Button';
77
import CIcon from 'src/domains/misc/components/CIcon';
@@ -52,8 +52,8 @@ const Toast = ({
5252

5353
return (
5454
<Container title="Toast">
55-
<IconContainer $fillColorToken={color}>
56-
<CIcon size={16} icon={icon} />
55+
<IconContainer>
56+
{status === 'inProgress' ? <Spinner icon="Spinner" size={16} /> : <CIcon size={16} icon={icon} color={color} />}
5757
</IconContainer>
5858
<RightSection>
5959
<Header>
@@ -111,16 +111,10 @@ const Container = styled.article`
111111
${boxShadows.shadow16}
112112
`;
113113

114-
const IconContainer = styled.div<{ $fillColorToken: string | undefined }>`
114+
const IconContainer = styled.div`
115115
display: grid;
116116
place-items: center;
117117
line-height: 0;
118-
119-
& * {
120-
${({ $fillColorToken }) => $fillColorToken && css`
121-
fill: ${$fillColorToken};
122-
`}
123-
}
124118
`;
125119

126120
const RightSection = styled.section`
@@ -218,3 +212,25 @@ const ProgressBar = styled.div`
218212
219213
transform-origin: center left;
220214
`;
215+
216+
const Spinner = styled(CIcon)`
217+
animation: spin 2s infinite linear;
218+
219+
path:first-of-type {
220+
fill: ${vars('--color-brand-stroke-2-contrast-rest')};
221+
}
222+
223+
path:last-of-type {
224+
fill: ${vars('--color-neutral-stroke-1-rest')};
225+
}
226+
227+
@keyframes spin {
228+
from {
229+
transform: rotate(0deg);
230+
}
231+
232+
to {
233+
transform: rotate(360deg);
234+
}
235+
}
236+
`;

src/domains/misc/components/Toast/consts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const STATUS_ICONS_DATA: Record<string, { icon: IconName, color: string |
1515
color: vars('--color-neutral-foreground-2-rest'),
1616
},
1717
error: {
18-
icon: 'Dismiss',
18+
icon: 'DismissCircle',
1919
color: vars('--color-status-danger-foreground-1-rest'),
2020
},
2121
inProgress: {

src/domains/shielder/utils/useShield.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { erc20Token, nativeToken } from '@cardinal-cryptography/shielder-sdk';
22
import { useMutation, useQueryClient } from '@tanstack/react-query';
3-
import { erc20Abi } from 'viem';
3+
import { useRef } from 'react';
4+
import { erc20Abi, TransactionExecutionError, UserRejectedRequestError } from 'viem';
45
import { useAccount, usePublicClient, useSendTransaction, useWalletClient } from 'wagmi';
56

67
import { Token } from 'src/domains/chains/types/misc';
78
import useChain from 'src/domains/chains/utils/useChain';
9+
import { useToast } from 'src/domains/misc/components/Toast';
810
import getQueryKey from 'src/domains/misc/utils/getQueryKey';
911

1012
import useShielderClient from './useShielderClient';
@@ -17,9 +19,51 @@ export const useShield = () => {
1719
const { sendTransactionAsync } = useSendTransaction();
1820
const queryClient = useQueryClient();
1921
const chainConfig = useChain();
22+
const { showToast } = useToast();
23+
const rejectedTxRef = useRef(false);
24+
25+
const sendTransactionWithToast = async (params: Parameters<typeof sendTransactionAsync>[0]) => {
26+
const toast = showToast({
27+
status: 'inProgress',
28+
title: 'Transaction pending',
29+
subtitle: 'Waiting to be signed by user.',
30+
ttlMs: Infinity,
31+
});
32+
33+
const timeoutId = setTimeout(() => {
34+
toast.updateToast({
35+
subtitle: 'Still waiting? Make sure you signed the transaction from your wallet.',
36+
});
37+
}, 30_000);
38+
39+
try {
40+
const tx = await sendTransactionAsync(params);
41+
clearTimeout(timeoutId);
42+
toast.dismissToast();
43+
return tx;
44+
} catch (error) {
45+
clearTimeout(timeoutId);
46+
toast.dismissToast();
47+
48+
const isRejected = error instanceof TransactionExecutionError && error.cause instanceof UserRejectedRequestError;
49+
50+
if(isRejected) {
51+
rejectedTxRef.current = true;
52+
showToast({
53+
status: 'error',
54+
title: 'Transaction rejected',
55+
subtitle: 'Transaction has been rejected in the wallet',
56+
});
57+
}
58+
59+
throw error;
60+
}
61+
};
2062

2163
const { mutateAsync: shield, isPending: isShielding, ...meta } = useMutation({
2264
mutationFn: async ({ token, amount }: { token: Token, amount: bigint, onSuccess?: () => void }) => {
65+
rejectedTxRef.current = false;
66+
2367
if (!shielderClient) throw new Error('Shielder is not ready');
2468
if (!walletAddress) throw new Error('Address is not available');
2569

@@ -50,7 +94,7 @@ export const useShield = () => {
5094
await shielderClient.shield(
5195
sdkToken,
5296
amount,
53-
async params => await sendTransactionAsync(params),
97+
sendTransactionWithToast,
5498
walletAddress
5599
);
56100
},
@@ -75,6 +119,13 @@ export const useShield = () => {
75119
void queryClient.invalidateQueries({
76120
queryKey: getQueryKey.tokenPublicBalance('native', chainId.toString(), walletAddress),
77121
});
122+
123+
if (!rejectedTxRef.current) {
124+
showToast({
125+
status: 'error',
126+
title: 'Shielding failed',
127+
});
128+
}
78129
},
79130
});
80131

0 commit comments

Comments
 (0)