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

TransactionTracker Component #58

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
05bf0c7
TransactionTracker component refactor
mikestarrdev Sep 8, 2023
a55ca8e
refactored helper functions into separate files for Staking flow
mikestarrdev Sep 8, 2023
18eb16b
refactored and renamed files
mikestarrdev Sep 8, 2023
cd4bffd
remove unnecessary variables from helper functions
mikestarrdev Sep 8, 2023
ebacbd6
refactored functions and changed types
mikestarrdev Sep 9, 2023
a4af210
added staking-flow CodeTour file with description of functions
mikestarrdev Sep 10, 2023
9f4c4d0
added displayManageStake prop to ManageStake. Modified CodeTour file
mikestarrdev Sep 10, 2023
32cdc84
commented out loadingSpinner from TransactionTracker
mikestarrdev Sep 10, 2023
d6c1e3b
fixed type error in renderIcon
mikestarrdev Sep 10, 2023
df7c205
refactor interfaces and component props to be more consistent
mikestarrdev Sep 12, 2023
75605fd
fix naming of hooks
mikestarrdev Sep 13, 2023
9961d96
fix naming of useUnstakeSast
mikestarrdev Sep 13, 2023
0bbc2b5
resolve merge conflicts, remove loading spinner from button
mikestarrdev Sep 14, 2023
bbe4439
changed tracker message descriptions
mikestarrdev Sep 14, 2023
834299a
fixed button status text
mikestarrdev Sep 14, 2023
20a84a7
updated staking-flow.tour file
mikestarrdev Sep 14, 2023
c1e65a1
Refactored dialog into Modal. useStakingModalStore refactor
mikestarrdev Sep 15, 2023
5d63f8b
refactor TransactionTracker and StakingModal components
mikestarrdev Sep 17, 2023
c28ad2f
Fixed variables to pass in as props to TransactionTracker, and fixed …
mikestarrdev Sep 18, 2023
7be9f0f
fixed button actions and text in TransactionTracker
mikestarrdev Sep 18, 2023
c62912f
moved react-hook-form useForm hook into Zustand store
mikestarrdev Sep 18, 2023
1f08a68
moved useForm outside of Zustand. Updated staking-flow codetour file
mikestarrdev Sep 19, 2023
65af651
merge from main
dmosites Sep 20, 2023
041d56d
Merge branch 'main' into fix/transaction-tracker-component
mikestarrdev Sep 20, 2023
fec5f5a
Disabled close on outside modal click, headline in walletconnection m…
mikestarrdev Sep 20, 2023
24395ba
wallet connection fixes
mikestarrdev Sep 21, 2023
13baa22
added loading state headlines for modal
mikestarrdev Sep 21, 2023
5cc960e
remove redundant variables. update staking-flow.tour file
mikestarrdev Sep 21, 2023
12a123f
resolve merge conflicts
mikestarrdev Sep 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .tours/staking-flow.tour
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"$schema": "https://aka.ms/codetour-schema",
"title": "Staking flow",
"steps": [
{
"file": "src/features/staking/StakingModal.tsx",
"description": "Values imported from Zustand store:\n- formReturn manages state of NumberInput, where the user enters the amount to approve/stake/unstake\n- txType is either \"stake\" or \"unstake\"",
"line": 20
},
{
"file": "src/features/staking/StakingModal.tsx",
"description": "Checks if user needs to approve token spending. If `needsApproval` is `true`, clicking the stake button will fire off `approveAst` function. If `needsApproval` is `false`, the connected user has approved the contract to spend AST and can stake tokens",
"line": 33
},
{
"file": "src/features/staking/StakingModal.tsx",
"description": "useApproveAst, useStakeAst, and useUnstakeSast are all similar function.\n- approveAst: this is a 'write' function that approves the contract to spend AST\n- dataApproveAst: the 'data' functions in these hooks are mostly used to get a transaction hash after the 'write' function runs.\n- restApproveAst: the 'reset' functions can get called to reset the data after a 'write' function gets called. Resetting data allows for the TransactionTracker.tsx component to render the correct data based on where a user is in the staking flow",
"line": 43
},
{
"file": "src/features/staking/StakingModal.tsx",
"description": "- `txStatus` returns the current state of a transaction: \"idle\", \"loading\", \"success\", or \"error\". These values get passed into AmountStakedText.tsx, which then gets passed into TransactionTracker.tsx. These values help show the user where they are in the staking flow.\n- `useWaitForTransaction` hook gets called in StakingModal.tsx instead of TransactionTracker.tsx, because we want to keep TransactionTracker as generic as possible, to allow for non-staking transactions to also be passed in",
"line": 61
},
{
"file": "src/features/staking/StakingModal.tsx",
"description": "`modalButtonActionsAndText` returns functions based on where a user is in the staking flow. These are 'write' functions from Wagmi hooks that get called when a user clicks the \"STAKE\" button in StakingModal.tsx. These button actions are different than the `actionButtons` variable below; `actionButtons` returns 'reset' functions from Wagmi, which help the TransactionTracker.tsx component get \"unstuck\" when a user moves along the staking flow.",
"line": 66
},
{
"file": "src/features/staking/StakingModal.tsx",
"description": "AmountStakedText has its own component because it contains flex box formatting, and does not need to be rendered unless a transaction is successful",
"line": 86
},
{
"file": "src/features/staking/StakingModal.tsx",
"description": "Variable gets passed as a prop to TransactionTracker. Gives user a description of what's currently happening if a transaction is processing",
"line": 88
},
{
"file": "src/features/staking/StakingModal.tsx",
"description": "function returns 'reset' functions from Wagmi",
"line": 95
},
{
"file": "src/features/staking/StakingModal.tsx",
"description": "`modalTxLoadingStateHeadlines` updates the headline in Modal.tsx to reflect where the user is in the staking flow process",
"line": 101
},
{
"file": "src/features/staking/StakingModal.tsx",
"description": "Logic processes `actionButtons` and passes the correct functions as a prop to TransactionTracker. These functions get called after a transaction finishes processing, and allows a user to move along to the next step in the staking flow.",
"line": 108
},
{
"file": "src/features/staking/StakingModal.tsx",
"description": "ManageStake.tsx renders \"stake\" and \"unstake\" buttons, and NumberInput.",
"line": 135
}
]
}
15 changes: 2 additions & 13 deletions src/features/chain-connection/WalletConnectionModal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Dispatch, SetStateAction, useEffect } from "react";
import { MdClose } from "react-icons/md";
import { twJoin } from "tailwind-merge";
import { Connector, useAccount, useConnect } from "wagmi";
import { LineBreak } from "../common/LineBreak";
import { Modal } from "../common/Modal";
import coinbaseWalletLogo from "./assets/wallet-logos/coinbase-wallet.svg";
import frameLogo from "./assets/wallet-logos/frame-logo.png";
Expand Down Expand Up @@ -35,20 +33,11 @@ const WalletConnectionModal = ({

return (
<Modal
className={twJoin("text-white")}
className="text-white"
modalHeadline="Select Wallet"
onCloseRequest={() => setShowConnectionModal(false)}
>
<div className="color-white flex w-[360px] flex-col bg-gray-900 font-bold">
<div className="flex justify-between items-center mb-4 -mt-2">
<h2 className="font-semibold text-xl">Select Wallet</h2>
<div
className="hover:cursor-pointer"
onClick={() => setShowConnectionModal(false)}
>
<MdClose className="text-gray-500" size={26} />
</div>
</div>
<LineBreak className="mb-4 -mx-6" />
<div className="flex flex-col gap-2">
{connectorsList
// .filter((connector) => connector.ready)
Expand Down
26 changes: 7 additions & 19 deletions src/features/common/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { ReactNode } from "react";
import { ImSpinner8 } from "react-icons/im";
import { twMerge } from "tailwind-merge";
import { tv, type VariantProps } from "tailwind-variants";
import { StakeOrUnstake, Status } from "../staking/types/StakingTypes";
import { buttonLoadingSpinner } from "../staking/utils/helpers";

// TODO: this button needs more work, including
// - hover states, pressed states, focus-visible states, etc.
Expand All @@ -17,6 +14,7 @@ const buttonVariants = tv({
color: {
primary: "bg-airswap-blue text-white hover:border-white",
transparent: "border border-gray-800 hover:border-white",
black: "bg-black border border-gray-800 hover:border-white",
},
rounded: {
true: "rounded-full",
Expand All @@ -39,19 +37,10 @@ const buttonVariants = tv({

type ButtonVariants = VariantProps<typeof buttonVariants>;

type LoadingSpinnerArgs = {
stakeOrUnstake: StakeOrUnstake;
needsApproval: boolean;
statusApprove: Status;
statusStake: Status;
statusUnstake: Status;
};

interface ButtonProps {
children?: ReactNode;
className?: string;
isDisabled?: boolean;
loadingSpinnerArgs?: LoadingSpinnerArgs;
}

export const Button = ({
Expand All @@ -60,25 +49,24 @@ export const Button = ({
rounded,
size,
color,
isDisabled = false,
loadingSpinnerArgs,
isDisabled,
...rest
}: ButtonProps &
React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> &
ButtonVariants) => {
const isLoadingSpinner =
loadingSpinnerArgs && buttonLoadingSpinner(loadingSpinnerArgs);

return (
<button
className={twMerge(buttonVariants({ color, rounded, size }), className)}
className={twMerge(
buttonVariants({ color, rounded, size }),
className,
isDisabled && "opacity-70",
)}
{...rest}
disabled={isDisabled}
>
{isLoadingSpinner && <ImSpinner8 className="animate-spin mr-2 " />}
{children}
</button>
);
Expand Down
20 changes: 20 additions & 0 deletions src/features/common/EtherscanUrl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { IoMdOpen } from "react-icons/io";
import { Hash } from "viem";
import { useNetwork } from "wagmi";
import { etherscanLink } from "../staking/utils/etherscanLink";

export const EtherscanUrl = (transactionHash: Hash | undefined) => {
const { chain } = useNetwork();
const blockExplorerLink = etherscanLink(chain?.id, transactionHash);

return (
<a
href={blockExplorerLink}
target="_"
className="flex flex-row items-center text-font-darkSubtext"
>
<span className="mr-1">View on Etherscan</span>
<IoMdOpen />
</a>
);
};
11 changes: 11 additions & 0 deletions src/features/common/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { useKeyboardEvent } from "@react-hookz/web";
import { useEffect, useRef } from "react";
import { MdClose } from "react-icons/md";
import { twMerge } from "tailwind-merge";
import { LineBreak } from "./LineBreak";

export const Modal = ({
className,
modalHeadline,
onCloseRequest,
children,
}: {
className?: string;
modalHeadline?: string;
onCloseRequest: () => void;
children?: React.ReactNode;
}) => {
Expand Down Expand Up @@ -37,6 +41,13 @@ export const Modal = ({
)}
>
<div className="px-6 py-7 bg-gray-900 border border-[#1F2937] rounded-lg">
<div className="flex justify-between items-center mb-4 -mt-2">
<h2 className="font-semibold text-xl">{modalHeadline}</h2>
<div className="hover:cursor-pointer" onClick={onCloseRequest}>
<MdClose className="text-gray-500" size={26} />
</div>
</div>
<LineBreak className="mb-4 -mx-6" />
{children}
</div>
</dialog>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,69 +1,53 @@
import { Dispatch, FC } from "react";
import { FieldValues, UseFormReturn } from "react-hook-form";
import { twJoin } from "tailwind-merge";
import AirSwapLogo from "../../../assets/airswap-logo.svg";
import { useTokenBalances } from "../../../hooks/useTokenBalances";
import { Button } from "../../common/Button";
import { LineBreak } from "../../common/LineBreak";
import { StakeOrUnstake, Status } from "../types/StakingTypes";
import AirSwapLogo from "../../assets/airswap-logo.svg";
import { useTokenBalances } from "../../hooks/useTokenBalances";
import { Button } from "../common/Button";
import { LineBreak } from "../common/LineBreak";
import { NumberInput } from "./NumberInput";
import { StakableBar } from "./StakableBar";
import { TxType, useStakingModalStore } from "./store/useStakingModalStore";

interface ManageStakeProps {
formReturn: UseFormReturn<FieldValues>;
stakeOrUnstake: StakeOrUnstake;
setStakeOrUnstake: Dispatch<StakeOrUnstake>;
statusApprove: Status;
statusStake: Status;
statusUnstake: Status;
}

export const ManageStake: FC<ManageStakeProps> = ({
export const ManageStake = ({
formReturn,
stakeOrUnstake,
setStakeOrUnstake,
statusApprove,
statusStake,
statusUnstake,
}: {
formReturn: UseFormReturn<FieldValues>;
}) => {
const { txType, setTxType } = useStakingModalStore();
const { getValues } = formReturn;
const stakingAmount = getValues().stakingAmount;

const {
astBalanceFormatted: astBalance,
ustakableSAstBalanceFormatted: unstakableSAstBalance,
} = useTokenBalances();

const isTransactionLoading =
statusApprove === "loading" ||
statusStake === "loading" ||
statusUnstake === "loading";

return (
<>
<LineBreak className="relative -mx-6" />
<div>
<StakableBar className="my-6" />
<LineBreak className="relative mb-4 -mx-6" />
<div className="font-lg pointer-cursor rounded-md font-semibold">
<Button
className={twJoin([
"w-1/2 p-2",
`${stakeOrUnstake === "stake" ? "bg-gray-800" : "text-gray-500"}`,
`${txType === "stake" ? "bg-gray-800" : "text-gray-500"}`,
])}
rounded="leftFalse"
size="small"
onClick={() => setStakeOrUnstake(StakeOrUnstake.STAKE)}
disabled={isTransactionLoading}
onClick={() => setTxType(TxType.STAKE)}
>
Stake
</Button>
<Button
className={twJoin(
"w-1/2 p-2",
`${stakeOrUnstake === "unstake" ? "bg-gray-800" : "text-gray-500"}`,
`${txType === "unstake" ? "bg-gray-800" : "text-gray-500"}`,
)}
rounded="rightFalse"
size="small"
color="transparent"
onClick={() => setStakeOrUnstake(StakeOrUnstake.UNSTAKE)}
disabled={isTransactionLoading}
onClick={() => setTxType(TxType.UNSTAKE)}
disabled={stakingAmount <= 0}
>
Unstake
</Button>
Expand All @@ -83,24 +67,17 @@ export const ManageStake: FC<ManageStakeProps> = ({
<div className="flex flex-col items-end uppercase w-full overflow-hidden">
<div>
<NumberInput
stakeOrUnstake={stakeOrUnstake}
astBalance={+astBalance}
unstakableSAstBalance={+unstakableSAstBalance}
formReturn={formReturn}
name="stakingAmount"
isDisabled={!!isTransactionLoading}
/>
</div>
<span className="text-xs font-medium leading-4 text-gray-500">
{stakeOrUnstake === StakeOrUnstake.STAKE
? astBalance
: unstakableSAstBalance}{" "}
{stakeOrUnstake === StakeOrUnstake.STAKE
? "stakable"
: "unstakable"}
{txType === TxType.STAKE ? astBalance : unstakableSAstBalance}{" "}
{txType === TxType.STAKE ? "stakable" : "unstakable"}
</span>
</div>
</div>
</>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,50 +1,38 @@
import { FieldValues, UseFormReturn } from "react-hook-form";
import { twJoin } from "tailwind-merge";
import { StakeOrUnstake } from "../types/StakingTypes";
import { TxType, useStakingModalStore } from "./store/useStakingModalStore";

export const NumberInput = ({
stakeOrUnstake,
astBalance,
unstakableSAstBalance,
formReturn,
name,
isDisabled,
}: {
stakeOrUnstake: StakeOrUnstake;
astBalance: number;
unstakableSAstBalance: number;
formReturn: UseFormReturn<FieldValues>;
name: string;
isDisabled: boolean;
}) => {
const { txType } = useStakingModalStore();
const { register, setValue } = formReturn;

return (
<input
max={astBalance}
placeholder="0"
defaultValue="0"
{...register("stakingAmount", {
valueAsNumber: true,
required: true,
min: 0,
max:
stakeOrUnstake === StakeOrUnstake.STAKE
? astBalance
: unstakableSAstBalance,
max: txType === TxType.STAKE ? astBalance : unstakableSAstBalance,
validate: (val) => !isNaN(val) && val > 0,
onChange: (e) => {
if (isNaN(e.target.value) && e.target.value !== ".") {
setValue(name, "");
setValue("stakingAmount", "");
}

setValue(name, e.target.value);
setValue("stakingAmount", e.target.value);
},
})}
// FIXME: monospace font per designs.
className={twJoin(
"items-right w-1/5 bg-transparent text-right text-white min-w-fit font-medium text-[20px]",
)}
disabled={isDisabled}
className="items-right w-1/5 bg-transparent text-right text-white min-w-fit font-medium text-[20px]"
/>
);
};
Loading