Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: session key demo: improve timers, styles, and error handling #1368

Merged
merged 9 commits into from
Feb 20, 2025
11 changes: 5 additions & 6 deletions examples/ui-demo/src/components/small-cards/MintCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,11 @@ const MintCardInner = ({
<LoadingIcon />
)}
</div>
<div>
<div className="mb-2">
<h3 className="text-fg-primary text-base xl:text-xl font-semibold">
Gasless transactions
</h3>
</div>
<div className="w-full mb-3">
<h3 className="text-fg-primary xl:text-xl font-semibold mb-2 xl:mb-3">
Gasless transactions
</h3>

{!mintStarted ? (
<>
<p className="text-fg-primary text-sm mb-3">
Expand Down
40 changes: 25 additions & 15 deletions examples/ui-demo/src/components/small-cards/Transactions.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { ExternalLinkIcon } from "@/components/icons/external-link";
import { CheckCircleFilledIcon } from "@/components/icons/check-circle-filled";
import { LoadingIcon } from "@/components/icons/loading";
import {
RECURRING_TXN_INTERVAL,
TransactionType,
} from "@/hooks/useRecurringTransactions";
import { cn } from "@/lib/utils";
import { useEffect, useState } from "react";
import { TransactionType } from "@/hooks/useRecurringTransactions";

export type loadingState = "loading" | "success" | "initial";

Expand All @@ -25,41 +29,47 @@ const Transaction = ({
externalLink,
buyAmountUsdc,
state,
timeToBuy,
}: TransactionType & { className?: string }) => {
const [countdownSeconds, setCountdownSeconds] = useState<number>(10);
const [secUntilBuy, setSecUntilBuy] = useState<number | undefined>(undefined);

useEffect(() => {
if (state === "next") {
const interval = setInterval(() => {
setCountdownSeconds((prev) => (prev === 0 ? 0 : prev - 1));
}, 1000);
return () => clearInterval(interval);
} else {
setCountdownSeconds(10);
if (state === "complete" || state === "initial" || !timeToBuy) {
return;
}
}, [state]);

const interval = setInterval(() => {
setSecUntilBuy(Math.ceil((timeToBuy - Date.now()) / 1000));
}, 250);

return () => clearInterval(interval);
}, [state, timeToBuy]);

const getText = () => {
if (state === "initial") {
return "Waiting...";
}
if (state === "next") {
return secUntilBuy != null && secUntilBuy <= 0
? "Waiting for previous transaction..."
: `Next buy in ${secUntilBuy ?? RECURRING_TXN_INTERVAL / 1000} second${
secUntilBuy === 1 ? "" : "s"
}`;
}
if (state === "initiating") {
return "Buying 1 ETH";
}
if (state === "next") {
return `Next buy in ${countdownSeconds} seconds`;
}
if (state === "complete") {
return `Bought 1 ETH for ${buyAmountUsdc.toLocaleString()} USDC`;
}
};

return (
<div className={`flex justify-between ${className} mb-4`}>
<div className={cn("flex justify-between mb-4", className)}>
<div className="flex items-center mr-1">
<div className="w-4 h-4 mr-2">
{state === "complete" ? (
<CheckCircleFilledIcon className=" h-4 w-4 fill-demo-surface-success" />
<CheckCircleFilledIcon className="h-4 w-4 fill-demo-surface-success" />
) : (
<LoadingIcon className="h-4 w-4" />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,16 @@ const TransactionsCardInner = ({
}, [cardStatus]);

return (
<div className="bg-bg-surface-default rounded-lg p-4 w-full xl:p-6 xl:w-[326px] xl:h-[478px] flex flex-col shadow-smallCard mb-5 xl:mb-0">
<div className="flex gap-3 xl:gap-0 xl:flex-col">
<div className="flex-shrink-0 bg-[#EAEBFE] rounded-xl mb-4 flex justify-center items-center relative h-[67px] w-[60px] sm:h-[154px] sm:w-[140px] xl:h-[222px] xl:w-full">
<div className="bg-bg-surface-default rounded-lg p-4 xl:p-6 w-full xl:w-[326px] xl:h-[478px] flex flex-col shadow-smallCard">
<div className="flex xl:flex-col gap-4">
<div className="flex-shrink-0 bg-[#EAEBFE] rounded-xl sm:mb-3 xl:mb-0 flex justify-center items-center relative h-[67px] w-[60px] sm:h-[154px] sm:w-[140px] xl:h-[222px] xl:w-full">
<p className="absolute top-[-6px] left-[-6px] sm:top-1 sm:left-1 xl:left-auto xl:right-4 xl:top-4 px-2 py-1 font-semibold rounded-md text-xs text-[#7c3AED] bg-[#F3F3FF]">
New!
</p>
<Key className="h-9 w-9 sm:h-[74px] sm:w-[74px] xl:h-[94px] xl:w-[94px]" />
</div>
<div className="mb-3">
<h3 className="text-fg-primary xl:text-xl font-semibold mb-2 xl:mb-3">
<div className="mb-3 w-full">
<h3 className="text-fg-primary xl:text-xl font-semibold mb-2 xl:mb-3">
Recurring transactions
</h3>

Expand All @@ -71,17 +71,15 @@ const TransactionsCardInner = ({
) : cardStatus === "setup" ? (
<div className="flex items-center">
<LoadingIcon className="h-4 w-4 mr-2" />
<p className="text-fg-primary text-sm">
Creating session key and minting USDC...
</p>
<p className="text-fg-primary text-sm">Creating session key...</p>
</div>
) : (
<Transactions transactions={transactions} />
)}
</div>
</div>
<Button
className="mt-auto"
className="mt-auto w-full"
onClick={handleTransactions}
disabled={
isLoadingClient || cardStatus === "setup" || cardStatus === "active"
Expand Down
2 changes: 1 addition & 1 deletion examples/ui-demo/src/components/small-cards/Wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const SmallCardsWrapper = () => {
const { walletType } = useConfigStore();

return (
<div className="flex flex-col xl:flex-row gap-6 lg:mt-6 items-center p-6">
<div className="flex flex-col xl:flex-row gap-6 lg:mt-6 items-center p-6 w-full justify-center max-w-screen-sm xl:max-w-none">
{walletType === WalletTypes.smart ? (
<>
<MintCardDefault />
Expand Down
105 changes: 61 additions & 44 deletions examples/ui-demo/src/hooks/useRecurringTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { swapAbi } from "./7702/dca/abi/swap";
import { erc20MintableAbi } from "./7702/dca/abi/erc20Mintable";
import { genEntityId } from "./7702/genEntityId";
import { SESSION_KEY_VALIDITY_TIME_SECONDS } from "./7702/constants";
import { useToast } from "@/hooks/useToast";
import { AlchemyTransport } from "@account-kit/infra";

export type CardStatus = "initial" | "setup" | "active" | "done";
Expand All @@ -33,11 +34,12 @@ export type TransactionType = {
state: TransactionStages;
buyAmountUsdc: number;
externalLink?: string;
timeToBuy?: number; // timestamp when the txn should initiate
};

export const initialTransactions: TransactionType[] = [
{
state: "initiating",
state: "initial",
buyAmountUsdc: 4000,
},
{
Expand All @@ -50,6 +52,8 @@ export const initialTransactions: TransactionType[] = [
},
];

export const RECURRING_TXN_INTERVAL = 10_000;

export interface UseRecurringTransactionReturn {
isLoadingClient: boolean;
cardStatus: CardStatus;
Expand Down Expand Up @@ -84,22 +88,38 @@ export const useRecurringTransactions = (clientOptions: {
},
});

const handleTransaction = async (transactionIndex: number) => {
setTransactions((prev) => {
const newState = [...prev];
newState[transactionIndex].state = "initiating";
if (transactionIndex + 1 < newState.length) {
newState[transactionIndex + 1].state = "next";
}
return newState;
const { setToast } = useToast();

const handleError = (error: Error) => {
console.error(error);
setCardStatus("initial");
setTransactions(initialTransactions);
setToast({
type: "error",
text: "Something went wrong. Please try again.",
open: true,
});
};

const handleTransaction = async (transactionIndex: number) => {
if (!sessionKeyClient) {
console.error("no session key client");
setCardStatus("initial");
return;
return handleError(new Error("no session key client"));
}

setTransactions((prev) =>
prev.map((txn, idx) =>
idx === transactionIndex
? { ...txn, state: "initiating" }
: idx === transactionIndex + 1
? {
...txn,
state: "next",
timeToBuy: Date.now() + RECURRING_TXN_INTERVAL,
}
: txn
)
);

const usdcInAmount = transactions[transactionIndex].buyAmountUsdc;

const uoHash = await sessionKeyClient.sendUserOperation({
Expand All @@ -116,47 +136,36 @@ export const useRecurringTransactions = (clientOptions: {
const txnHash = await sessionKeyClient
.waitForUserOperationTransaction(uoHash)
.catch((e) => {
console.log(e);
console.error(e);
});

if (!txnHash) {
setCardStatus("initial");
return;
return handleError(new Error("missing swap txn hash"));
}

setTransactions((prev) => {
const newState = [...prev];
newState[transactionIndex].state = "complete";
newState[transactionIndex].externalLink = clientOptions.chain
.blockExplorers
? `${clientOptions.chain.blockExplorers.default.url}/tx/${txnHash}`
: undefined;
return newState;
});
setTransactions((prev) =>
prev.map((txn, idx) =>
idx === transactionIndex
? {
...txn,
state: "complete",
externalLink: clientOptions.chain.blockExplorers
? `${clientOptions.chain.blockExplorers.default.url}/tx/${txnHash}`
: undefined,
}
: txn
)
);
};

// Mock method to fire transactions for 7702
// Mock method to fire transactions
const handleTransactions = async () => {
if (!client) {
console.error("no client");
return;
}

// initial state as referenced by `const initialTransactions` is mutated, so we need to re-create it.
setTransactions([
{
state: "initiating",
buyAmountUsdc: 4000,
},
{
state: "initial",
buyAmountUsdc: 3500,
},
{
state: "initial",
buyAmountUsdc: 4200,
},
]);
setTransactions(initialTransactions);
setCardStatus("setup");

// Start by minting the required USDC amount, and installing the session key, if not already installed.
Expand Down Expand Up @@ -255,19 +264,27 @@ export const useRecurringTransactions = (clientOptions: {
const txnHash = await client
.waitForUserOperationTransaction(uoHash)
.catch((e) => {
console.log(e);
console.error(e);
});

if (!txnHash) {
setCardStatus("initial");
return;
return handleError(new Error("missing batch txn hash"));
}

setSessionKeyAdded(true);
setCardStatus("active");

for (let i = 0; i < transactions.length; i++) {
await handleTransaction(i);
await Promise.all([
handleTransaction(i),
...(i < transactions.length - 1
? [
new Promise((resolve) =>
setTimeout(resolve, RECURRING_TXN_INTERVAL)
),
]
: []),
]);
}

setCardStatus("done");
Expand Down