Skip to content

Commit 1727dcd

Browse files
authored
Merge pull request #70 from Cardinal-Cryptography/sd-69-show-pending-transaction-toast
SD-69: Show pending transaction toast
2 parents 1f510e1 + f0a28fc commit 1727dcd

File tree

6 files changed

+127
-18
lines changed

6 files changed

+127
-18
lines changed

public/chains/84532.svg

Lines changed: 10 additions & 2 deletions
Loading

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: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { useAccount, usePublicClient, useSendTransaction, useWalletClient } from
55

66
import { Token } from 'src/domains/chains/types/misc';
77
import useChain from 'src/domains/chains/utils/useChain';
8+
import { useToast } from 'src/domains/misc/components/Toast';
89
import getQueryKey from 'src/domains/misc/utils/getQueryKey';
10+
import { getWalletErrorName, handleWalletError } from 'src/domains/shielder/utils/walletErrors';
911

1012
import useShielderClient from './useShielderClient';
1113

@@ -17,6 +19,31 @@ export const useShield = () => {
1719
const { sendTransactionAsync } = useSendTransaction();
1820
const queryClient = useQueryClient();
1921
const chainConfig = useChain();
22+
const { showToast } = useToast();
23+
24+
const sendTransactionWithToast = async (params: Parameters<typeof sendTransactionAsync>[0]) => {
25+
const toast = showToast({
26+
status: 'inProgress',
27+
title: 'Transaction pending',
28+
subtitle: 'Waiting to be signed by user.',
29+
ttlMs: Infinity,
30+
});
31+
32+
const timeoutId = setTimeout(() => {
33+
toast.updateToast({
34+
subtitle: 'Still waiting? Make sure you signed the transaction from your wallet.',
35+
});
36+
}, 10_000);
37+
38+
try {
39+
return await sendTransactionAsync(params);
40+
} catch (error) {
41+
return handleWalletError(error);
42+
} finally {
43+
clearTimeout(timeoutId);
44+
toast.dismissToast();
45+
}
46+
};
2047

2148
const { mutateAsync: shield, isPending: isShielding, ...meta } = useMutation({
2249
mutationFn: async ({ token, amount }: { token: Token, amount: bigint, onSuccess?: () => void }) => {
@@ -50,7 +77,7 @@ export const useShield = () => {
5077
await shielderClient.shield(
5178
sdkToken,
5279
amount,
53-
async params => await sendTransactionAsync(params),
80+
sendTransactionWithToast,
5481
walletAddress
5582
);
5683
},
@@ -75,6 +102,32 @@ export const useShield = () => {
75102
void queryClient.invalidateQueries({
76103
queryKey: getQueryKey.tokenPublicBalance('native', chainId.toString(), walletAddress),
77104
});
105+
106+
const knowErrorName = getWalletErrorName(error);
107+
108+
switch (knowErrorName) {
109+
case 'USER_REJECTED_REQUEST':
110+
showToast({
111+
status: 'error',
112+
title: 'Transaction rejected',
113+
subtitle: 'Transaction has been rejected in the wallet',
114+
});
115+
break;
116+
117+
case 'LOCKED_OR_UNAUTHORIZED':
118+
showToast({
119+
status: 'error',
120+
title: 'Transaction not initiated',
121+
subtitle: 'Make sure your wallet is unlocked and your account is authorized.',
122+
});
123+
break;
124+
125+
default:
126+
showToast({
127+
status: 'error',
128+
title: 'Shielding failed',
129+
});
130+
}
78131
},
79132
});
80133

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { DefaultError } from '@tanstack/react-query';
2+
import { TransactionExecutionError } from 'viem';
3+
4+
const ERROR_NAME_BY_CODE = {
5+
4001: 'USER_REJECTED_REQUEST',
6+
4100: 'LOCKED_OR_UNAUTHORIZED',
7+
} as const;
8+
9+
type KnownErrorCode = keyof typeof ERROR_NAME_BY_CODE;
10+
type KnownErrorName = (typeof ERROR_NAME_BY_CODE)[keyof typeof ERROR_NAME_BY_CODE];
11+
12+
export const handleWalletError = (error: unknown): never => {
13+
if (error instanceof TransactionExecutionError) {
14+
const cause = error.cause as { code?: number };
15+
const code = cause.code?.toString() as KnownErrorCode | undefined;
16+
const knownErrorName = code ? ERROR_NAME_BY_CODE[code] : undefined;
17+
18+
if (knownErrorName) {
19+
throw new Error(knownErrorName);
20+
}
21+
throw error;
22+
}
23+
throw error;
24+
};
25+
26+
export const getWalletErrorName = (error: DefaultError): KnownErrorName | null => {
27+
const matches = Object.values(ERROR_NAME_BY_CODE).find(name =>
28+
error.message.includes(name)
29+
);
30+
31+
return matches ?? null;
32+
};

0 commit comments

Comments
 (0)