Skip to content

Commit

Permalink
fix: session key demo: improve timers, styles, and error handling (#1368
Browse files Browse the repository at this point in the history
)

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

* chore: build docs

* chore: lint

* chore: improvements to intervals

* chore: run prettier
  • Loading branch information
jakehobbs authored Feb 20, 2025
1 parent a29c95a commit 6855610
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 75 deletions.
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
16 changes: 7 additions & 9 deletions examples/ui-demo/src/components/small-cards/TransactionsCard.tsx
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

0 comments on commit 6855610

Please # to comment.