diff --git a/packages/nextjs/components/Batch/BatchContainer.tsx b/packages/nextjs/components/Batch/BatchContainer.tsx index e8ccad3b..febcefca 100644 --- a/packages/nextjs/components/Batch/BatchContainer.tsx +++ b/packages/nextjs/components/Batch/BatchContainer.tsx @@ -292,6 +292,8 @@ export default function BatchContainer() { proposeBatch, isLoading: isProposing, loadingState, + loadingStep, + totalSteps, } = useBatchTransaction({ onSuccess: async () => { setSelectedItems(new Set()); @@ -439,6 +441,8 @@ export default function BatchContainer() { onConfirm={handleProposeBatch} isLoading={isProposing} loadingState={loadingState} + loadingStep={loadingStep} + totalSteps={totalSteps} accountId={accountId} /> @@ -452,6 +456,8 @@ export default function BatchContainer() { onConfirm={handleProposeBatch} isLoading={isProposing} loadingState={loadingState} + loadingStep={loadingStep} + totalSteps={totalSteps} accountId={accountId} /> diff --git a/packages/nextjs/components/Batch/TransactionSummary.tsx b/packages/nextjs/components/Batch/TransactionSummary.tsx index 97121066..a27b84b0 100644 --- a/packages/nextjs/components/Batch/TransactionSummary.tsx +++ b/packages/nextjs/components/Batch/TransactionSummary.tsx @@ -19,6 +19,8 @@ interface TransactionSummaryProps { isLoading?: boolean; loadingState?: string; accountId: string | null; + loadingStep?: number; + totalSteps?: number; } const TransactionSummary: React.FC = ({ @@ -28,6 +30,8 @@ const TransactionSummary: React.FC = ({ isLoading = false, loadingState = "", accountId, + loadingStep = 0, + totalSteps = 4, }) => { const { data: contacts = [] } = useContacts(accountId); return ( @@ -116,13 +120,26 @@ const TransactionSummary: React.FC = ({ {/* Confirm Button Section */}
+ {isLoading && loadingState && loadingStep > 0 && ( +
+
+ Step {loadingStep} of {totalSteps} — {loadingState} +
+
+
+
+
+ )}
diff --git a/packages/nextjs/components/Batch/TransactionSummaryDrawer.tsx b/packages/nextjs/components/Batch/TransactionSummaryDrawer.tsx index 7b17e2f1..8f24dbfe 100644 --- a/packages/nextjs/components/Batch/TransactionSummaryDrawer.tsx +++ b/packages/nextjs/components/Batch/TransactionSummaryDrawer.tsx @@ -19,6 +19,8 @@ interface TransactionSummaryDrawerProps { onConfirm?: () => void; isLoading?: boolean; loadingState?: string; + loadingStep?: number; + totalSteps?: number; } export const TransactionSummaryDrawer = memo(function TransactionSummaryDrawer({ @@ -29,6 +31,8 @@ export const TransactionSummaryDrawer = memo(function TransactionSummaryDrawer({ onConfirm, isLoading = false, loadingState = "", + loadingStep = 0, + totalSteps = 4, }: TransactionSummaryDrawerProps) { const [isAnimating, setIsAnimating] = useState(false); @@ -68,6 +72,8 @@ export const TransactionSummaryDrawer = memo(function TransactionSummaryDrawer({ accountId={accountId} isLoading={isLoading} loadingState={loadingState} + loadingStep={loadingStep} + totalSteps={totalSteps} className="h-full" />
diff --git a/packages/nextjs/components/Dashboard/TransactionRow.tsx b/packages/nextjs/components/Dashboard/TransactionRow.tsx index ba7bfef3..1b19adf0 100644 --- a/packages/nextjs/components/Dashboard/TransactionRow.tsx +++ b/packages/nextjs/components/Dashboard/TransactionRow.tsx @@ -632,7 +632,17 @@ export function TransactionRow({ tx, onSuccess }: TransactionRowProps) { // Get totalSigners realtime from wallet commitments const totalSigners = commitmentsData?.length || 0; - const { approve, deny, execute, isLoading: loading, loadingState } = useTransactionVote({ onSuccess }); + const { + approve, + deny, + execute, + isLoading: loading, + loadingState, + loadingStep, + totalSteps, + } = useTransactionVote({ + onSuccess, + }); const handleApprove = async () => { await approve(tx); @@ -718,7 +728,16 @@ export function TransactionRow({ tx, onSuccess }: TransactionRowProps) {
{/* Loading State */} {loading && loadingState && ( -
{loadingState}
+
+
+ {loadingStep > 0 && totalSteps > 1 && ( + + Step {loadingStep}/{totalSteps} + + )} + {loadingState} +
+
)} {/* Main Container */} diff --git a/packages/nextjs/components/Transfer/TransferContainer.tsx b/packages/nextjs/components/Transfer/TransferContainer.tsx index be46f515..e8e603b9 100644 --- a/packages/nextjs/components/Transfer/TransferContainer.tsx +++ b/packages/nextjs/components/Transfer/TransferContainer.tsx @@ -6,6 +6,7 @@ import { ResolvedToken, parseTokenAmount } from "@polypay/shared"; import { parseEther } from "viem"; import { ContactPicker } from "~~/components/contact-book/ContactPicker"; import { TokenPillPopover } from "~~/components/popovers/TokenPillPopover"; +import { Spinner } from "~~/components/ui/Spinner"; import { useMetaMultiSigWallet, useTransferTransaction } from "~~/hooks"; import { useCreateBatchItem } from "~~/hooks/api"; import { useNetworkTokens } from "~~/hooks/app/useNetworkTokens"; @@ -69,7 +70,7 @@ export default function TransferContainer() { } }, [form]); - const { transfer, isLoading, loadingState } = useTransferTransaction({ + const { transfer, isLoading, loadingState, loadingStep, totalSteps } = useTransferTransaction({ onSuccess: () => { form.reset(); setSelectedContactId(null); @@ -145,15 +146,15 @@ export default function TransferContainer() { return (
{/* Background images */} -
+
Top globe
-
+
Bottom globe
{/* Main content */} -
+
{/* Title section */}
transfering
@@ -164,11 +165,6 @@ export default function TransferContainer() {
- {/* Loading state */} - {isLoading && loadingState && ( -
{loadingState}
- )} -
{/* Action buttons */} + {isLoading && loadingState && ( +
+
+ Step {loadingStep} of {totalSteps} — {loadingState} +
+
+
+
+
+ )}
diff --git a/packages/nextjs/components/modals/EditAccountModal/EditStep.tsx b/packages/nextjs/components/modals/EditAccountModal/EditStep.tsx index 8d39a2a0..3546dcdf 100644 --- a/packages/nextjs/components/modals/EditAccountModal/EditStep.tsx +++ b/packages/nextjs/components/modals/EditAccountModal/EditStep.tsx @@ -24,7 +24,6 @@ const EditStep: React.FC = ({ existingSigners, originalThreshold, loading, - loadingState, onNext, onClose, }) => { @@ -174,11 +173,6 @@ const EditStep: React.FC = ({
- {/* Loading state */} - {loading && loadingState && ( -
{loadingState}
- )} - {/* Content */}
{/* Account Signers Section */} diff --git a/packages/nextjs/components/modals/EditAccountModal/SubmittingStep.tsx b/packages/nextjs/components/modals/EditAccountModal/SubmittingStep.tsx index 21058d66..2044a136 100644 --- a/packages/nextjs/components/modals/EditAccountModal/SubmittingStep.tsx +++ b/packages/nextjs/components/modals/EditAccountModal/SubmittingStep.tsx @@ -2,7 +2,13 @@ import React from "react"; -const SubmittingStep = () => { +interface SubmittingStepProps { + loadingState?: string; + loadingStep?: number; + totalSteps?: number; +} + +const SubmittingStep: React.FC = ({ loadingState = "", loadingStep = 0, totalSteps = 4 }) => { return (
{/* Rocket animation video */} @@ -30,6 +36,20 @@ const SubmittingStep = () => { This may take a few moments. Please don't close this window.

+ + {loadingStep > 0 && totalSteps > 0 && ( +
+
+ Step {loadingStep} of {totalSteps} — {loadingState} +
+
+
+
+
+ )}
); }; diff --git a/packages/nextjs/components/modals/EditAccountModal/index.tsx b/packages/nextjs/components/modals/EditAccountModal/index.tsx index 4de1fe8d..f5fe4c52 100644 --- a/packages/nextjs/components/modals/EditAccountModal/index.tsx +++ b/packages/nextjs/components/modals/EditAccountModal/index.tsx @@ -25,6 +25,8 @@ const EditAccountModal: React.FC = ({ isOpen, onClose }) => { updateThreshold, isLoading: loading, loadingState, + loadingStep, + totalSteps, signers, threshold: originalThreshold, refetchCommitments, @@ -190,7 +192,9 @@ const EditAccountModal: React.FC = ({ isOpen, onClose }) => { /> )} - {step === "submitting" && } + {step === "submitting" && ( + + )} ); }; diff --git a/packages/nextjs/components/ui/Spinner.tsx b/packages/nextjs/components/ui/Spinner.tsx new file mode 100644 index 00000000..b1293298 --- /dev/null +++ b/packages/nextjs/components/ui/Spinner.tsx @@ -0,0 +1,7 @@ +interface SpinnerProps { + className?: string; +} + +export function Spinner({ className = "h-4 w-4" }: SpinnerProps) { + return
; +} diff --git a/packages/nextjs/hooks/app/transaction/useBatchTransaction.ts b/packages/nextjs/hooks/app/transaction/useBatchTransaction.ts index 1d9700c4..e31faa58 100644 --- a/packages/nextjs/hooks/app/transaction/useBatchTransaction.ts +++ b/packages/nextjs/hooks/app/transaction/useBatchTransaction.ts @@ -4,6 +4,7 @@ import { useWalletClient } from "wagmi"; import { useMetaMultiSigWallet } from "~~/hooks"; import { useCreateTransaction, useReserveNonce } from "~~/hooks/api"; import { useGenerateProof } from "~~/hooks/app/useGenerateProof"; +import { useStepLoading } from "~~/hooks/app/useStepLoading"; import { useIdentityStore } from "~~/services/store"; import { formatErrorMessage } from "~~/utils/formatError"; import { notification } from "~~/utils/scaffold-eth"; @@ -12,9 +13,16 @@ interface UseBatchTransactionOptions { onSuccess?: () => void; } +const BATCH_STEPS = [ + { id: 1, label: "Preparing your batch..." }, + { id: 2, label: "Waiting for wallet approval..." }, + { id: 3, label: "Securing your transaction..." }, + { id: 4, label: "Almost done, submitting..." }, +]; + export const useBatchTransaction = (options?: UseBatchTransactionOptions) => { - const [isLoading, setIsLoading] = useState(false); - const [loadingState, setLoadingState] = useState(""); + const { isLoading, loadingState, loadingStep, totalSteps, startStep, setStepByLabel, reset } = + useStepLoading(BATCH_STEPS); const { data: walletClient } = useWalletClient(); const { secret, commitment: myCommitment } = useIdentityStore(); @@ -22,7 +30,7 @@ export const useBatchTransaction = (options?: UseBatchTransactionOptions) => { const { mutateAsync: createTransaction } = useCreateTransaction(); const { mutateAsync: reserveNonce } = useReserveNonce(); const { generateProof } = useGenerateProof({ - onLoadingStateChange: setLoadingState, + onLoadingStateChange: setStepByLabel, }); const proposeBatch = async (selectedBatchItems: BatchItem[]) => { @@ -42,16 +50,15 @@ export const useBatchTransaction = (options?: UseBatchTransactionOptions) => { return; } - setIsLoading(true); - try { const selectedIds = selectedBatchItems.map(item => item.id); // 1. Reserve nonce from backend + startStep(1); const { nonce } = await reserveNonce(metaMultiSigWallet.address); // 2. Get current threshold and commitments - setLoadingState("Preparing batch transaction..."); + startStep(1); const currentThreshold = await metaMultiSigWallet.read.signaturesRequired(); // 3. Prepare batch data @@ -79,7 +86,7 @@ export const useBatchTransaction = (options?: UseBatchTransactionOptions) => { const { proof, publicInputs, nullifier, vk } = await generateProof(txHash); // 7. Submit to backend - setLoadingState("Submitting to backend..."); + startStep(4); const result = await createTransaction({ nonce, type: TxType.BATCH, @@ -104,8 +111,7 @@ export const useBatchTransaction = (options?: UseBatchTransactionOptions) => { console.error("Propose batch error:", error); notification.error(formatErrorMessage(error, "Failed to propose batch")); } finally { - setIsLoading(false); - setLoadingState(""); + reset(); } }; @@ -113,5 +119,7 @@ export const useBatchTransaction = (options?: UseBatchTransactionOptions) => { proposeBatch, isLoading, loadingState, + loadingStep, + totalSteps, }; }; diff --git a/packages/nextjs/hooks/app/transaction/useSignerTransaction.ts b/packages/nextjs/hooks/app/transaction/useSignerTransaction.ts index 5128c5ea..7d917c47 100644 --- a/packages/nextjs/hooks/app/transaction/useSignerTransaction.ts +++ b/packages/nextjs/hooks/app/transaction/useSignerTransaction.ts @@ -3,6 +3,7 @@ import { SignerData, TxType, encodeAddSigners, encodeRemoveSigners, encodeUpdate import { useWalletClient } from "wagmi"; import { useGenerateProof, useMetaMultiSigWallet, useWalletCommitments, useWalletThreshold } from "~~/hooks"; import { useCreateTransaction, useReserveNonce } from "~~/hooks/api/useTransaction"; +import { useStepLoading } from "~~/hooks/app/useStepLoading"; import { formatErrorMessage } from "~~/utils/formatError"; import { notification } from "~~/utils/scaffold-eth"; @@ -10,14 +11,21 @@ interface UseSignerTransactionOptions { onSuccess?: () => void; } +const SIGNER_STEPS = [ + { id: 1, label: "Preparing your proposal..." }, + { id: 2, label: "Waiting for wallet approval..." }, + { id: 3, label: "Securing your transaction..." }, + { id: 4, label: "Almost done, submitting..." }, +]; + export const useSignerTransaction = (options?: UseSignerTransactionOptions) => { - const [isLoading, setIsLoading] = useState(false); - const [loadingState, setLoadingState] = useState(""); + const { isLoading, loadingState, loadingStep, totalSteps, startStep, setStepByLabel, reset } = + useStepLoading(SIGNER_STEPS); const { data: walletClient } = useWalletClient(); const metaMultiSigWallet = useMetaMultiSigWallet(); const { generateProof } = useGenerateProof({ - onLoadingStateChange: setLoadingState, + onLoadingStateChange: setStepByLabel, }); const { mutateAsync: createTransaction } = useCreateTransaction(); const { mutateAsync: reserveNonce } = useReserveNonce(); @@ -44,6 +52,8 @@ export const useSignerTransaction = (options?: UseSignerTransactionOptions) => { throw new Error("Wallet not connected"); } + startStep(1); + const { nonce } = await reserveNonce(metaMultiSigWallet.address); const currentThreshold = await metaMultiSigWallet.read.signaturesRequired(); @@ -56,7 +66,7 @@ export const useSignerTransaction = (options?: UseSignerTransactionOptions) => { const { proof, publicInputs, nullifier, vk } = await generateProof(txHash); - setLoadingState("Submitting to backend..."); + startStep(4); await createTransaction({ nonce, @@ -91,7 +101,6 @@ export const useSignerTransaction = (options?: UseSignerTransactionOptions) => { return; } - setIsLoading(true); try { const commitments = newSigners.map(s => s.commitment.trim()); const callData = encodeAddSigners(commitments, newThreshold); @@ -110,8 +119,7 @@ export const useSignerTransaction = (options?: UseSignerTransactionOptions) => { console.error("Failed to add signer:", error); notification.error(formatErrorMessage(error, "Failed to add signer")); } finally { - setIsLoading(false); - setLoadingState(""); + reset(); } }; @@ -143,7 +151,6 @@ export const useSignerTransaction = (options?: UseSignerTransactionOptions) => { return; } - setIsLoading(true); try { const commitments = signersToRemove.map(s => s.commitment.trim()); const callData = encodeRemoveSigners(commitments, adjustedThreshold); @@ -162,8 +169,7 @@ export const useSignerTransaction = (options?: UseSignerTransactionOptions) => { console.error("Failed to remove signer:", error); notification.error(formatErrorMessage(error, "Failed to remove signer")); } finally { - setIsLoading(false); - setLoadingState(""); + reset(); } }; @@ -178,7 +184,6 @@ export const useSignerTransaction = (options?: UseSignerTransactionOptions) => { return; } - setIsLoading(true); try { const callData = encodeUpdateThreshold(newThreshold); await executeSignerTransaction(TxType.SET_THRESHOLD, callData, { @@ -191,8 +196,7 @@ export const useSignerTransaction = (options?: UseSignerTransactionOptions) => { console.error("Failed to update threshold:", error); notification.error(formatErrorMessage(error, "Failed to update threshold")); } finally { - setIsLoading(false); - setLoadingState(""); + reset(); } }; @@ -202,6 +206,8 @@ export const useSignerTransaction = (options?: UseSignerTransactionOptions) => { updateThreshold, isLoading, loadingState, + loadingStep, + totalSteps, signers, threshold, refetchCommitments, diff --git a/packages/nextjs/hooks/app/transaction/useTransactionVote.ts b/packages/nextjs/hooks/app/transaction/useTransactionVote.ts index cda599e7..404688a6 100644 --- a/packages/nextjs/hooks/app/transaction/useTransactionVote.ts +++ b/packages/nextjs/hooks/app/transaction/useTransactionVote.ts @@ -16,6 +16,7 @@ import { useWalletClient } from "wagmi"; import { accountKeys, useMetaMultiSigWallet, userKeys } from "~~/hooks"; import { useApproveTransaction, useDenyTransaction, useExecuteTransaction } from "~~/hooks/api/useTransaction"; import { useGenerateProof } from "~~/hooks/app/useGenerateProof"; +import { useStepLoading } from "~~/hooks/app/useStepLoading"; import { useIdentityStore } from "~~/services/store/useIdentityStore"; import { formatErrorMessage } from "~~/utils/formatError"; import { notification } from "~~/utils/scaffold-eth"; @@ -117,9 +118,16 @@ function buildTransactionParams(tx: TransactionRowData): { return { to, value, callData }; } +const APPROVE_STEPS = [ + { id: 1, label: "Preparing approval..." }, + { id: 2, label: "Waiting for wallet approval..." }, + { id: 3, label: "Securing your transaction..." }, + { id: 4, label: "Almost done, submitting..." }, +]; + export const useTransactionVote = (options?: UseTransactionVoteOptions) => { - const [isLoading, setIsLoading] = useState(false); - const [loadingState, setLoadingState] = useState(""); + const { isLoading, loadingState, loadingStep, totalSteps, startStep, setStepByLabel, reset, startLoading } = + useStepLoading(APPROVE_STEPS); const { commitment } = useIdentityStore(); const { data: walletClient } = useWalletClient(); @@ -129,7 +137,7 @@ export const useTransactionVote = (options?: UseTransactionVoteOptions) => { const { mutateAsync: denyApi } = useDenyTransaction(); const { mutateAsync: executeApi } = useExecuteTransaction(); const { generateProof } = useGenerateProof({ - onLoadingStateChange: setLoadingState, + onLoadingStateChange: setStepByLabel, }); const queryClient = useQueryClient(); @@ -144,7 +152,8 @@ export const useTransactionVote = (options?: UseTransactionVoteOptions) => { return; } - setIsLoading(true); + // Full 4-step flow + startStep(1); try { // 1. Build callData based on tx type const { to, value, callData } = buildTransactionParams(tx); @@ -161,7 +170,7 @@ export const useTransactionVote = (options?: UseTransactionVoteOptions) => { const proofData = await generateProof(txHash); // 4. Submit to backend - setLoadingState("Submitting to backend..."); + startStep(4); await approveApi({ txId: tx.txId, dto: { @@ -179,8 +188,7 @@ export const useTransactionVote = (options?: UseTransactionVoteOptions) => { console.error("Approve error:", error); notification.error(formatErrorMessage(error, "Failed to approve")); } finally { - setIsLoading(false); - setLoadingState(""); + reset(); } }; @@ -195,9 +203,10 @@ export const useTransactionVote = (options?: UseTransactionVoteOptions) => { return; } - setIsLoading(true); + // Single-step fast action + startLoading("Submitting deny vote..."); try { - setLoadingState("Submitting deny vote..."); + // Fast single-step action; keep step 1 await denyApi({ txId: tx.txId, dto: { @@ -211,8 +220,7 @@ export const useTransactionVote = (options?: UseTransactionVoteOptions) => { console.error("Deny error:", error); notification.error(formatErrorMessage(error, "Failed to deny")); } finally { - setIsLoading(false); - setLoadingState(""); + reset(); } }; @@ -222,9 +230,9 @@ export const useTransactionVote = (options?: UseTransactionVoteOptions) => { return; } - setIsLoading(true); + // Single-step fast action + startLoading("Executing on-chain..."); try { - setLoadingState("Executing on-chain..."); const result = await executeApi({ txId, dto: { @@ -244,8 +252,7 @@ export const useTransactionVote = (options?: UseTransactionVoteOptions) => { console.error("Execute error:", error); notification.error(formatErrorMessage(error, "Failed to execute")); } finally { - setIsLoading(false); - setLoadingState(""); + reset(); } }; @@ -255,5 +262,7 @@ export const useTransactionVote = (options?: UseTransactionVoteOptions) => { execute, isLoading, loadingState, + loadingStep, + totalSteps, }; }; diff --git a/packages/nextjs/hooks/app/transaction/useTransferTransaction.ts b/packages/nextjs/hooks/app/transaction/useTransferTransaction.ts index 4c6d2b75..b41e2762 100644 --- a/packages/nextjs/hooks/app/transaction/useTransferTransaction.ts +++ b/packages/nextjs/hooks/app/transaction/useTransferTransaction.ts @@ -1,10 +1,11 @@ -import { useState } from "react"; +import { useEffect } from "react"; import { ResolvedToken, TxType, ZERO_ADDRESS, encodeERC20Transfer, parseTokenAmount } from "@polypay/shared"; import { parseEther } from "viem"; import { useWalletClient } from "wagmi"; import { useMetaMultiSigWallet } from "~~/hooks"; import { useCreateTransaction, useReserveNonce } from "~~/hooks/api/useTransaction"; import { useGenerateProof } from "~~/hooks/app/useGenerateProof"; +import { useStepLoading } from "~~/hooks/app/useStepLoading"; import { formatErrorMessage } from "~~/utils/formatError"; import { notification } from "~~/utils/scaffold-eth"; @@ -19,16 +20,23 @@ interface UseTransferTransactionOptions { onSuccess?: () => void; } +const TRANSFER_STEPS = [ + { id: 1, label: "Preparing your transfer..." }, + { id: 2, label: "Waiting for wallet approval..." }, + { id: 3, label: "Securing your transaction..." }, + { id: 4, label: "Almost done, submitting..." }, +]; + export const useTransferTransaction = (options?: UseTransferTransactionOptions) => { - const [isLoading, setIsLoading] = useState(false); - const [loadingState, setLoadingState] = useState(""); + const { isLoading, loadingState, loadingStep, totalSteps, startStep, setStepByLabel, reset } = + useStepLoading(TRANSFER_STEPS); const { data: walletClient } = useWalletClient(); const metaMultiSigWallet = useMetaMultiSigWallet(); const { mutateAsync: createTransaction } = useCreateTransaction(); const { mutateAsync: reserveNonce } = useReserveNonce(); const { generateProof } = useGenerateProof({ - onLoadingStateChange: setLoadingState, + onLoadingStateChange: setStepByLabel, }); const transfer = async ({ recipient, amount, token, contactId }: TransferParams) => { @@ -38,14 +46,13 @@ export const useTransferTransaction = (options?: UseTransferTransactionOptions) } const isNativeETH = token.address === ZERO_ADDRESS; - - setIsLoading(true); try { // 1. Reserve nonce from backend + startStep(1); const { nonce } = await reserveNonce(metaMultiSigWallet.address); // 2. Get current threshold and commitments - setLoadingState("Preparing transaction..."); + startStep(1); const currentThreshold = await metaMultiSigWallet.read.signaturesRequired(); // 3. Parse amount based on token type @@ -79,7 +86,7 @@ export const useTransferTransaction = (options?: UseTransferTransactionOptions) const { proof, publicInputs, nullifier, vk } = await generateProof(txHash); // 6. Submit to backend - setLoadingState("Submitting to backend..."); + startStep(4); const result = await createTransaction({ nonce, type: TxType.TRANSFER, @@ -105,8 +112,7 @@ export const useTransferTransaction = (options?: UseTransferTransactionOptions) console.error("Transfer error:", error); notification.error(formatErrorMessage(error, "Failed to create transfer")); } finally { - setIsLoading(false); - setLoadingState(""); + reset(); } }; @@ -114,5 +120,7 @@ export const useTransferTransaction = (options?: UseTransferTransactionOptions) transfer, isLoading, loadingState, + loadingStep, + totalSteps, }; }; diff --git a/packages/nextjs/hooks/app/useGenerateProof.ts b/packages/nextjs/hooks/app/useGenerateProof.ts index 92d17554..8b003dc5 100644 --- a/packages/nextjs/hooks/app/useGenerateProof.ts +++ b/packages/nextjs/hooks/app/useGenerateProof.ts @@ -75,7 +75,7 @@ export function useGenerateProof(options?: UseGenerateProofOptions) { } // 2. Sign txHash - setLoadingState("Signing transaction..."); + setLoadingState("Waiting for wallet approval..."); const signature = await walletClient.signMessage({ message: { raw: txHash }, }); @@ -115,7 +115,7 @@ export function useGenerateProof(options?: UseGenerateProofOptions) { const execResult = await noir.execute(input); // 7. Generate proof - setLoadingState("Generating ZK proof..."); + setLoadingState("Securing your transaction..."); const plonk = new UltraPlonkBackend(bytecode, { threads: 2 }); const { proof, publicInputs } = await plonk.generateProof(execResult.witness); // const vk = await plonk.getVerificationKey(); diff --git a/packages/nextjs/hooks/app/useStepLoading.ts b/packages/nextjs/hooks/app/useStepLoading.ts new file mode 100644 index 00000000..9285661d --- /dev/null +++ b/packages/nextjs/hooks/app/useStepLoading.ts @@ -0,0 +1,67 @@ +import { useEffect, useState } from "react"; + +export interface StepDefinition { + id: number; + label: string; +} + +export function useStepLoading(steps: StepDefinition[]) { + const [isLoading, setIsLoading] = useState(false); + const [loadingState, setLoadingState] = useState(""); + const [loadingStep, setLoadingStep] = useState(0); + + const totalSteps = steps.length; + + const startStep = (id: number) => { + const found = steps.find(s => s.id === id); + if (found) { + setIsLoading(true); + setLoadingStep(found.id); + setLoadingState(found.label); + } + }; + + const setStepByLabel = (label: string) => { + const found = steps.find(s => s.label === label); + if (found) { + setLoadingStep(found.id); + setLoadingState(found.label); + } else { + setLoadingState(label); + } + }; + + const reset = () => { + setIsLoading(false); + setLoadingStep(0); + setLoadingState(""); + }; + + const startLoading = (label: string) => { + setIsLoading(true); + setLoadingState(label); + setLoadingStep(0); + }; + + // Prevent accidental refresh while loading + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (isLoading) { + e.preventDefault(); + } + }; + window.addEventListener("beforeunload", handleBeforeUnload); + return () => window.removeEventListener("beforeunload", handleBeforeUnload); + }, [isLoading]); + + return { + isLoading, + loadingState, + loadingStep, + totalSteps, + startStep, + setStepByLabel, + reset, + startLoading, + }; +} diff --git a/packages/nextjs/styles/globals.css b/packages/nextjs/styles/globals.css index e99c5c16..d4de78b5 100644 --- a/packages/nextjs/styles/globals.css +++ b/packages/nextjs/styles/globals.css @@ -188,6 +188,16 @@ /* margin-bottom: 0.5rem; */ line-height: 1; } + + /* Override browser autofill styles */ + input:-webkit-autofill, + input:-webkit-autofill:hover, + input:-webkit-autofill:focus, + input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 1000px white inset !important; + -webkit-text-fill-color: var(--color-main-black) !important; + transition: 5000s ease-in-out 0s; + } } :root, diff --git a/packages/nextjs/utils/formatError.ts b/packages/nextjs/utils/formatError.ts index 0965a90b..983f18ba 100644 --- a/packages/nextjs/utils/formatError.ts +++ b/packages/nextjs/utils/formatError.ts @@ -7,7 +7,7 @@ type ErrorLike = { const ERROR_PATTERNS: [RegExp, string | null][] = [ [/user rejected|user denied|user refused|ACTION_REJECTED/i, "Transaction was cancelled."], - [/insufficient funds/i, "Insufficient funds for this transaction."], + [/insufficient (funds|balance|account)/i, "Insufficient funds for this transaction."], [/nonce too (high|low)/i, "Transaction conflict. Please try again."], [/gas required exceeds|cannot estimate gas/i, "Transaction failed. Please try again."], [/rate.?limit|429/i, "Too many requests. Please wait a moment."],