-
Notifications
You must be signed in to change notification settings - Fork 360
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* handle loading toast on checkout * chore: update deps * chore: revert deps update * fix:handle transaction cancellation
- Loading branch information
1 parent
10b94fb
commit d8cc3f7
Showing
7 changed files
with
393 additions
and
87 deletions.
There are no files selected for viewing
132 changes: 98 additions & 34 deletions
132
advanced/dapps/chain-abstraction-demo/app/hooks/useGiftDonut.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,64 +1,128 @@ | ||
"use client"; | ||
import { config } from "@/config"; | ||
import { tokenAddresses } from "@/consts/tokens"; | ||
import { Network, Token } from "@/data/EIP155Data"; | ||
import { toast } from "sonner"; | ||
import { erc20Abi, Hex } from "viem"; | ||
import { getAccount, getWalletClient } from "wagmi/actions"; | ||
import { erc20Abi, Hex, PublicClient } from "viem"; | ||
import { getAccount, getWalletClient, getPublicClient } from "wagmi/actions"; | ||
import { useState } from "react"; | ||
import { TransactionToast } from "@/components/TransactionToast"; | ||
|
||
type TransactionStatus = 'waiting-approval' | 'pending' | 'success' | 'error'; | ||
|
||
export default function useGiftDonut() { | ||
const [isPending, setIsPending] = useState(false); | ||
|
||
const updateToast = ( | ||
toastId: ReturnType<typeof toast>, | ||
status: TransactionStatus, | ||
{ elapsedTime, hash, networkName }: { | ||
elapsedTime?: number; | ||
hash?: string; | ||
networkName?: string; | ||
} = {} | ||
) => { | ||
toast( | ||
<TransactionToast | ||
status={status} | ||
elapsedTime={elapsedTime} | ||
hash={hash} | ||
networkName={networkName} | ||
/>, | ||
{ id: toastId } | ||
); | ||
}; | ||
|
||
const validateTransaction = async (network: Network) => { | ||
const client = await getWalletClient(config, { chainId: network.chainId }); | ||
const publicClient = getPublicClient(config); | ||
if (!publicClient) throw new Error("Failed to get public client"); | ||
|
||
const account = getAccount(config); | ||
const connectedChainId = account.chain?.id; | ||
|
||
if (!connectedChainId) throw new Error("Chain undefined"); | ||
if (connectedChainId !== network.chainId) { | ||
throw new Error("Please switch chain, connected chain does not match network"); | ||
} | ||
|
||
return { client, publicClient }; | ||
}; | ||
|
||
const getTokenContract = (token: Token, chainId: number) => { | ||
const tokenChainMapping = tokenAddresses[token.name]; | ||
if (!tokenChainMapping) throw new Error("Token not supported"); | ||
|
||
const contract = tokenChainMapping[chainId]; | ||
if (!contract) throw new Error("Can't send on specified chain"); | ||
|
||
return contract; | ||
}; | ||
|
||
const giftDonutAsync = async ( | ||
to: Hex, | ||
donutCount: number, | ||
token: Token, | ||
network: Network, | ||
) => { | ||
try { | ||
const client = await getWalletClient(config, { | ||
chainId: network.chainId, | ||
}); | ||
const account = getAccount(config); | ||
const connectedChainId = account.chain?.id; | ||
if (!connectedChainId) { | ||
throw new Error("Chain undefined"); | ||
} | ||
setIsPending(true); | ||
const startTime = Date.now(); | ||
const toastId = toast(<TransactionToast status="waiting-approval" />); | ||
let updateInterval: ReturnType<typeof setInterval>; | ||
|
||
if (connectedChainId !== network.chainId) { | ||
throw new Error("Please switch chain, connected chain does not match network"); | ||
} | ||
try { | ||
// Validate chain and get clients | ||
const { client, publicClient } = await validateTransaction(network); | ||
const chainId = getAccount(config).chain?.id!; | ||
|
||
const tokenName = token.name; | ||
const tokenChainMapping = tokenAddresses[tokenName]; | ||
if (!tokenChainMapping) { | ||
throw new Error("Token not supported"); | ||
} | ||
|
||
const contract = tokenChainMapping[connectedChainId]; | ||
if (!contract) { | ||
throw new Error("Cant send on specified chain"); | ||
} | ||
// Get token contract | ||
const contract = getTokenContract(token, chainId); | ||
const tokenAmount = donutCount * 1 * 10 ** 6; | ||
console.log({ to, tokenAmount, contract }); | ||
|
||
// Start tracking elapsed time | ||
updateInterval = setInterval(() => { | ||
updateToast(toastId, 'waiting-approval', { | ||
elapsedTime: Math.floor((Date.now() - startTime) / 1000) | ||
}); | ||
}, 1000); | ||
|
||
// Send transaction | ||
const tx = await client.writeContract({ | ||
abi: erc20Abi, | ||
address: contract, | ||
functionName: "transfer", | ||
args: [to, BigInt(tokenAmount)], | ||
}); | ||
toast.success(`Transaction sent with hash: ${tx}`); | ||
|
||
// Update to pending status | ||
updateToast(toastId, 'pending', { hash: tx, networkName: network.name }); | ||
|
||
// Wait for transaction | ||
const receipt = await publicClient.waitForTransactionReceipt({ hash: tx }); | ||
clearInterval(updateInterval); | ||
|
||
// Update final status | ||
const finalElapsedSeconds = Math.floor((Date.now() - startTime) / 1000); | ||
const finalStatus = receipt.status === 'success' ? 'success' : 'error'; | ||
|
||
updateToast(toastId, finalStatus, { | ||
elapsedTime: finalElapsedSeconds, | ||
hash: tx, | ||
networkName: network.name | ||
}); | ||
|
||
return tx; | ||
} catch (e) { | ||
|
||
clearInterval(updateInterval!); | ||
const finalElapsedSeconds = Math.floor((Date.now() - startTime) / 1000); | ||
|
||
if (e instanceof Error) { | ||
toast.error(e.message) | ||
} | ||
else { | ||
toast.error("Error sending gift donut"); | ||
updateToast(toastId, 'error', { elapsedTime: finalElapsedSeconds }); | ||
} | ||
console.error(e); | ||
} finally { | ||
setIsPending(false); | ||
} | ||
}; | ||
return { giftDonutAsync }; | ||
} | ||
|
||
return { giftDonutAsync, isPending }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
advanced/dapps/chain-abstraction-demo/components/CheckWalletToast.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import React from 'react'; | ||
import { Loader2 } from 'lucide-react'; | ||
|
||
export const CheckWalletToast = () => { | ||
return ( | ||
<div className="flex items-center gap-2"> | ||
<Loader2 className="h-4 w-4 animate-spin" /> | ||
<p className="text-sm font-medium">Check your wallet to approve transaction</p> | ||
</div> | ||
); | ||
}; |
72 changes: 72 additions & 0 deletions
72
advanced/dapps/chain-abstraction-demo/components/TransactionToast.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import React from 'react'; | ||
import { Loader2 } from 'lucide-react'; | ||
|
||
interface TransactionToastProps { | ||
hash?: string; | ||
networkName?: string; | ||
elapsedTime?: number; // in seconds | ||
status: 'waiting-approval' | 'pending' | 'success' | 'error'; | ||
} | ||
|
||
export const TransactionToast = ({ hash, networkName, elapsedTime, status }: TransactionToastProps) => { | ||
const formatTime = (seconds: number) => { | ||
if (seconds < 60) return `${seconds}s`; | ||
const minutes = Math.floor(seconds / 60); | ||
const remainingSeconds = seconds % 60; | ||
return `${minutes}m ${remainingSeconds}s`; | ||
}; | ||
|
||
const renderContent = () => { | ||
switch (status) { | ||
case 'waiting-approval': | ||
return ( | ||
<div className="flex items-center gap-2"> | ||
<Loader2 className="h-4 w-4 animate-spin" /> | ||
<p className="text-sm font-medium">Check your wallet to approve transaction</p> | ||
</div> | ||
); | ||
case 'pending': | ||
return ( | ||
<div className="flex items-center gap-2"> | ||
<Loader2 className="h-4 w-4 animate-spin" /> | ||
<div className="flex flex-col"> | ||
<p className="text-sm font-medium">Sending Gift Donut</p> | ||
{hash && networkName && ( | ||
<a | ||
href={`${networkName}/tx/${hash}`} | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
className="text-xs text-blue-500 hover:text-blue-600" | ||
> | ||
View transaction | ||
</a> | ||
)} | ||
{elapsedTime && ( | ||
<p className="text-xs text-secondary">Time elapsed: {formatTime(elapsedTime)}</p> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
case 'success': | ||
return ( | ||
<div className="flex flex-col"> | ||
<p className="text-sm font-medium">Transaction completed!</p> | ||
{elapsedTime && ( | ||
<p className="text-xs text-secondary">Completed in {formatTime(elapsedTime)}</p> | ||
)} | ||
</div> | ||
); | ||
case 'error': | ||
return ( | ||
<div className="flex flex-col"> | ||
<p className="text-sm font-medium">Transaction failed</p> | ||
{elapsedTime && ( | ||
<p className="text-xs text-secondary">Failed after {formatTime(elapsedTime)}</p> | ||
)} | ||
</div> | ||
); | ||
} | ||
}; | ||
|
||
return <div className="w-full">{renderContent()}</div>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.