Skip to content

Commit

Permalink
Implement flow confirmation modals
Browse files Browse the repository at this point in the history
  • Loading branch information
jmrossy committed Jan 23, 2024
1 parent 32dbb9a commit 4fa9665
Show file tree
Hide file tree
Showing 15 changed files with 244 additions and 109 deletions.
4 changes: 2 additions & 2 deletions src/components/icons/Checkmark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ function _Checkmark({
width={width}
height={height}
className={className}
viewBox="0 0 16 16"
viewBox="0 0 36 27"
>
<path
fill={fill}
d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"
d="M32.22 0 12.45 19.77l-7.03-5.62-2.08-1.67L0 16.65l10.98 8.79 1.86 1.47L34.11 5.64 36 3.78 32.22 0Z"
/>
</svg>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/nav/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function NavBar({ collapsed }: { collapsed?: boolean }) {
{l.to === pathname && (
<div
className={`absolute h-0.5 w-full bg-black transition-all duration-500 ${
collapsed ? '-bottom-3.5' : '-bottom-5'
collapsed ? '-bottom-3' : '-bottom-4'
}`}
></div>
)}
Expand Down
59 changes: 25 additions & 34 deletions src/components/notifications/TxSuccessToast.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,38 @@
import { useEffect } from 'react';
import { toast } from 'react-toastify';
import { ExternalLink } from 'src/components/buttons/ExternalLink';
import { ChainId, chainIdToChain } from 'src/config/chains';
import { ChainId } from 'src/config/chains';
import { getTxExplorerUrl } from 'src/features/transactions/utils';
import { logger } from 'src/utils/logger';

export function useToastTxSuccess(
isConfirmed?: boolean,
txHash?: string,
msg?: string,
chainId: ChainId = ChainId.Celo,
) {
useEffect(() => {
if (!isConfirmed || !txHash) return;
logger.debug(msg);
toastTxSuccess(txHash, msg, chainId);
}, [isConfirmed, txHash, msg, chainId]);
}

export function toastTxSuccess(
txHash?: string,
msg = 'Transaction confirmed!',
chainId: ChainId = ChainId.Celo,
) {
if (!txHash) return;
const explorerUrl = chainIdToChain[chainId].explorerUrl;
toast.success(<TxSuccessToast msg={msg} txHash={txHash} explorerUrl={explorerUrl} />, {
autoClose: 15000,
});
}

export function TxSuccessToast({
msg,
export function useToastTxSuccess({
isConfirmed,
txHash,
explorerUrl,
message = 'Transaction confirmed!',
chainId = ChainId.Celo,
enabled = true,
}: {
msg: string;
txHash: string;
explorerUrl: string;
isConfirmed?: boolean;
txHash?: string;
message?: string;
chainId?: ChainId;
enabled?: boolean;
}) {
useEffect(() => {
if (!isConfirmed || !txHash || !enabled) return;
logger.debug(message);
const explorerUrl = getTxExplorerUrl(txHash, chainId);
toast.success(<TxSuccessToast message={message} explorerUrl={explorerUrl} />, {
autoClose: 15000,
});
}, [isConfirmed, txHash, message, chainId, enabled]);
}

function TxSuccessToast({ message, explorerUrl }: { message: string; explorerUrl: string }) {
return (
<div>
{msg + ' '}
<ExternalLink className="underline" href={`${explorerUrl}/tx/${txHash}`}>
{message + ' '}
<ExternalLink className="underline" href={explorerUrl}>
See Details
</ExternalLink>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/features/account/AccountRegisterForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function AccountRegisterForm({
}) {
const { writeContract, isLoading } = useWriteContractWithReceipt(
'account registration',
refetchAccountDetails,
() => refetchAccountDetails,
);

const onClickCreate = () => {
Expand Down
27 changes: 18 additions & 9 deletions src/features/locking/LockFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,35 @@ import { AccountRegisterForm } from 'src/features/account/AccountRegisterForm';
import { useAccountDetails } from 'src/features/account/hooks';
import { LockForm } from 'src/features/locking/LockForm';
import { LockActionType } from 'src/features/locking/types';
import { TransactionConfirmation } from 'src/features/transactions/TransactionConfirmation';
import { useTransactionFlowConfirmation } from 'src/features/transactions/hooks';

import { isNullish } from 'src/utils/typeof';
import { useAccount } from 'wagmi';

export function LockFlow({ defaultAction }: { defaultAction?: LockActionType }) {
export function LockFlow({
defaultAction,
closeModal,
}: {
defaultAction?: LockActionType;
closeModal: () => void;
}) {
const { address } = useAccount();
const {
isRegistered,
isLoading: isLoadingRegistration,
refetch: refetchAccountDetails,
} = useAccountDetails(address);
const { isRegistered, refetch: refetchAccountDetails } = useAccountDetails(address);

const { confirmationDetails, onConfirmed } = useTransactionFlowConfirmation();

let Component;
if (!address || isLoadingRegistration || isNullish(isRegistered)) {
if (!address || isNullish(isRegistered)) {
Component = <SpinnerWithLabel className="py-20">Loading account data...</SpinnerWithLabel>;
} else if (!isRegistered) {
Component = <AccountRegisterForm refetchAccountDetails={refetchAccountDetails} />;
// TODO lock complete screen here
} else if (!confirmationDetails) {
Component = <LockForm defaultAction={defaultAction} onConfirmed={onConfirmed} />;
} else {
Component = <LockForm defaultAction={defaultAction} />;
Component = (
<TransactionConfirmation confirmation={confirmationDetails} closeModal={closeModal} />
);
}

return (
Expand Down
27 changes: 17 additions & 10 deletions src/features/locking/LockForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { lockedGoldABI } from '@celo/abis';
import { Form, Formik, FormikErrors, useFormikContext } from 'formik';
import { useEffect, useMemo } from 'react';
import { toast } from 'react-toastify';
Expand All @@ -7,7 +6,6 @@ import { AmountField } from 'src/components/input/AmountField';
import { RadioField } from 'src/components/input/RadioField';
import { TipBox } from 'src/components/layout/TipBox';
import { MIN_REMAINING_BALANCE } from 'src/config/consts';
import { Addresses } from 'src/config/contracts';
import { useBalance } from 'src/features/account/hooks';
import { useIsGovernanceVoting } from 'src/features/governance/useVotingStatus';
import { getLockTxPlan } from 'src/features/locking/lockPlan';
Expand All @@ -26,6 +24,7 @@ import {
import { StakingBalances } from 'src/features/staking/types';
import { emptyStakeBalances, useStakingBalances } from 'src/features/staking/useStakingBalances';
import { useTransactionPlan, useWriteContractWithReceipt } from 'src/features/transactions/hooks';
import { ConfirmationDetails } from 'src/features/transactions/types';
import { fromWeiRounded, toWei } from 'src/utils/amount';
import { logger } from 'src/utils/logger';
import { toTitleCase } from 'src/utils/strings';
Expand All @@ -40,9 +39,11 @@ const initialValues: LockFormValues = {
export function LockForm({
defaultAction,
showTip,
onConfirmed,
}: {
defaultAction?: LockActionType;
showTip?: boolean;
onConfirmed?: (details: ConfirmationDetails) => void;
}) {
const { address } = useAccount();
const { balance: walletBalance } = useBalance(address);
Expand All @@ -54,18 +55,24 @@ export function LockForm({
useTransactionPlan<LockFormValues>({
createTxPlan: (v) =>
getLockTxPlan(v, pendingWithdrawals || [], stakeBalances || emptyStakeBalances),
onStepSuccess: refetch,
onStepSuccess: () => refetch,
onPlanSuccess: onConfirmed
? (v, r) =>
onConfirmed({
message: `${v.action} successful`,
amount: v.amount,
receipt: r,
properties: [
{ label: 'Action', value: toTitleCase(v.action) },
{ label: 'Amount', value: `${v.amount} CELO` },
],
})
: undefined,
});
const { writeContract, isLoading } = useWriteContractWithReceipt('lock/unlock', onTxSuccess);
const isInputDisabled = isLoading || isPlanStarted;

const onSubmit = (values: LockFormValues) => {
writeContract({
address: Addresses.LockedGold,
abi: lockedGoldABI,
...getNextTx(values),
});
};
const onSubmit = (values: LockFormValues) => writeContract(getNextTx(values));

const validate = (values: LockFormValues) => {
if (isNullish(walletBalance) || !lockedBalances || !stakeBalances || isNullish(isVoting)) {
Expand Down
24 changes: 22 additions & 2 deletions src/features/locking/lockPlan.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { lockedGoldABI } from '@celo/abis';
import { Addresses } from 'src/config/contracts';
import { LockActionType, LockFormValues, PendingWithdrawal } from 'src/features/locking/types';
import { StakingBalances } from 'src/features/staking/types';
import { TxPlan } from 'src/features/transactions/types';
Expand All @@ -19,7 +21,15 @@ export function getLockTxPlan(

// TODO update this to account for staking, governance, and delegation revocations first
if (action === LockActionType.Unlock) {
return [{ action, functionName: 'unlock', args: [amountWei] }];
return [
{
action,
address: Addresses.LockedGold,
abi: lockedGoldABI,
functionName: 'unlock',
args: [amountWei],
},
];
} else if (action === LockActionType.Lock) {
const txs: TxPlan = [];
// Need relock from the pendings in reverse order
Expand All @@ -31,14 +41,22 @@ export function getLockTxPlan(
const txAmount = bigIntMin(amountRemaining, p.value);
txs.push({
action,
address: Addresses.LockedGold,
abi: lockedGoldABI,
functionName: 'relock',
args: [p.index, txAmount],
});
amountRemaining -= txAmount;
}
// If pending relocks didn't cover it
if (amountRemaining > 0) {
txs.push({ action, functionName: 'lock', value: amountRemaining });
txs.push({
action,
address: Addresses.LockedGold,
abi: lockedGoldABI,
functionName: 'lock',
value: amountRemaining,
});
}
return txs;
} else if (action === LockActionType.Withdraw) {
Expand All @@ -49,6 +67,8 @@ export function getLockTxPlan(
if (p.timestamp <= now)
txs.push({
action,
address: Addresses.LockedGold,
abi: lockedGoldABI,
functionName: 'withdraw',
args: [p.index],
});
Expand Down
39 changes: 21 additions & 18 deletions src/features/staking/StakeFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,48 @@ import { LockForm } from 'src/features/locking/LockForm';
import { StakeForm } from 'src/features/staking/StakeForm';
import { StakeActionType } from 'src/features/staking/types';
import { useStakingBalances } from 'src/features/staking/useStakingBalances';
import { TransactionConfirmation } from 'src/features/transactions/TransactionConfirmation';
import { useTransactionFlowConfirmation } from 'src/features/transactions/hooks';

import { isNullish } from 'src/utils/typeof';
import { useAccount } from 'wagmi';

export function StakeFlow({
defaultGroup,
defaultAction,
closeModal,
}: {
defaultGroup?: Address;
defaultAction?: StakeActionType;
closeModal: () => void;
}) {
const { address } = useAccount();
const { lockedBalance, isLoading: isLoadingLocked } = useLockedBalance(address);
const { stakeBalances, isLoading: isLoadingStaked } = useStakingBalances(address);
const {
isRegistered,
isLoading: isLoadingRegistration,
refetch: refetchAccountDetails,
} = useAccountDetails(address);
const { lockedBalance } = useLockedBalance(address);
const { stakeBalances } = useStakingBalances(address);
const { isRegistered, refetch: refetchAccountDetails } = useAccountDetails(address);

const { confirmationDetails, onConfirmed } = useTransactionFlowConfirmation();

let Component;
if (
!address ||
isLoadingLocked ||
isLoadingStaked ||
isLoadingRegistration ||
isNullish(lockedBalance) ||
isNullish(stakeBalances) ||
isNullish(isRegistered)
) {
if (!address || isNullish(lockedBalance) || isNullish(stakeBalances) || isNullish(isRegistered)) {
Component = <SpinnerWithLabel className="py-20">Loading staking data...</SpinnerWithLabel>;
} else if (!isRegistered) {
Component = <AccountRegisterForm refetchAccountDetails={refetchAccountDetails} />;
} else if (lockedBalance <= 0n && stakeBalances.total <= 0n) {
Component = <LockForm showTip={true} />;
} else if (!confirmationDetails) {
Component = (
<StakeForm
defaultGroup={defaultGroup}
defaultAction={defaultAction}
onConfirmed={onConfirmed}
/>
);
} else {
Component = <StakeForm defaultGroup={defaultGroup} defaultAction={defaultAction} />;
Component = (
<TransactionConfirmation confirmation={confirmationDetails} closeModal={closeModal} />
);
}
// TODO stake complete screen here

return (
<>
Expand Down
Loading

0 comments on commit 4fa9665

Please sign in to comment.