Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions packages/nextjs/components/Batch/BatchContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@ export default function BatchContainer() {
proposeBatch,
isLoading: isProposing,
loadingState,
loadingStep,
totalSteps,
} = useBatchTransaction({
onSuccess: async () => {
setSelectedItems(new Set());
Expand Down Expand Up @@ -439,6 +441,8 @@ export default function BatchContainer() {
onConfirm={handleProposeBatch}
isLoading={isProposing}
loadingState={loadingState}
loadingStep={loadingStep}
totalSteps={totalSteps}
accountId={accountId}
/>
</div>
Expand All @@ -452,6 +456,8 @@ export default function BatchContainer() {
onConfirm={handleProposeBatch}
isLoading={isProposing}
loadingState={loadingState}
loadingStep={loadingStep}
totalSteps={totalSteps}
accountId={accountId}
/>
</div>
Expand Down
19 changes: 18 additions & 1 deletion packages/nextjs/components/Batch/TransactionSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ interface TransactionSummaryProps {
isLoading?: boolean;
loadingState?: string;
accountId: string | null;
loadingStep?: number;
totalSteps?: number;
}

const TransactionSummary: React.FC<TransactionSummaryProps> = ({
Expand All @@ -28,6 +30,8 @@ const TransactionSummary: React.FC<TransactionSummaryProps> = ({
isLoading = false,
loadingState = "",
accountId,
loadingStep = 0,
totalSteps = 4,
}) => {
const { data: contacts = [] } = useContacts(accountId);
return (
Expand Down Expand Up @@ -116,13 +120,26 @@ const TransactionSummary: React.FC<TransactionSummaryProps> = ({

{/* Confirm Button Section */}
<div className="bg-grey-50 absolute bottom-0 left-0 right-0 px-5 py-4 border-t border-grey-200">
{isLoading && loadingState && loadingStep > 0 && (
<div className="flex flex-col items-center gap-2 w-full mb-3">
<div className="text-sm text-gray-500">
Step {loadingStep} of {totalSteps} — {loadingState}
</div>
<div className="w-full h-1.5 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 rounded-full transition-all duration-500 ease-out"
style={{ width: `${(loadingStep / totalSteps) * 100}%` }}
/>
</div>
</div>
)}
<button
onClick={onConfirm}
disabled={isLoading || transactions.length === 0}
className="flex items-center justify-center px-5 py-3 rounded-lg w-full disabled:opacity-50 bg-main-pink"
>
<span className="font-semibold text-sm text-center">
{isLoading ? loadingState || "Processing..." : `Execute`}
{isLoading ? loadingState || "Processing..." : `Submit batch`}
</span>
</button>
</div>
Expand Down
6 changes: 6 additions & 0 deletions packages/nextjs/components/Batch/TransactionSummaryDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ interface TransactionSummaryDrawerProps {
onConfirm?: () => void;
isLoading?: boolean;
loadingState?: string;
loadingStep?: number;
totalSteps?: number;
}

export const TransactionSummaryDrawer = memo(function TransactionSummaryDrawer({
Expand All @@ -29,6 +31,8 @@ export const TransactionSummaryDrawer = memo(function TransactionSummaryDrawer({
onConfirm,
isLoading = false,
loadingState = "",
loadingStep = 0,
totalSteps = 4,
}: TransactionSummaryDrawerProps) {
const [isAnimating, setIsAnimating] = useState(false);

Expand Down Expand Up @@ -68,6 +72,8 @@ export const TransactionSummaryDrawer = memo(function TransactionSummaryDrawer({
accountId={accountId}
isLoading={isLoading}
loadingState={loadingState}
loadingStep={loadingStep}
totalSteps={totalSteps}
className="h-full"
/>
</div>
Expand Down
23 changes: 21 additions & 2 deletions packages/nextjs/components/Dashboard/TransactionRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -718,7 +728,16 @@ export function TransactionRow({ tx, onSuccess }: TransactionRowProps) {
<div className="w-full mb-2">
{/* Loading State */}
{loading && loadingState && (
<div className="mb-1 px-4 py-2 bg-blue-50 text-blue-700 text-sm rounded-lg">{loadingState}</div>
<div className="mb-1 flex">
<div className="inline-flex items-center gap-2 bg-blue-50 text-blue-600 text-sm px-4 py-1 rounded-full">
{loadingStep > 0 && totalSteps > 1 && (
<span className="font-medium">
Step {loadingStep}/{totalSteps}
</span>
)}
<span>{loadingState}</span>
</div>
</div>
)}

{/* Main Container */}
Expand Down
35 changes: 23 additions & 12 deletions packages/nextjs/components/Transfer/TransferContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -145,15 +146,15 @@ export default function TransferContainer() {
return (
<div className="overflow-hidden relative w-full h-full flex flex-col rounded-lg">
{/* Background images */}
<div className="absolute -top-70 flex h-[736.674px] items-center justify-center left-1/2 translate-x-[-50%] w-[780px] pointer-events-none">
<div className="absolute -top-70 flex h-[736.674px] items-center justify-center left-1/2 translate-x-[-50%] w-[780px] pointer-events-none z-0">
<Image src="/transfer/top-globe.svg" alt="Top globe" className="w-full h-full" width={780} height={736} />
</div>
<div className="absolute -bottom-70 flex h-[736.674px] items-center justify-center left-1/2 translate-x-[-50%] w-[780px] pointer-events-none">
<div className="absolute -bottom-70 flex h-[736.674px] items-center justify-center left-1/2 translate-x-[-50%] w-[780px] pointer-events-none z-0">
<Image src="/transfer/bottom-globe.svg" alt="Bottom globe" className="w-full h-full" width={780} height={736} />
</div>

{/* Main content */}
<div className="flex flex-col gap-6 items-center justify-center flex-1 px-4">
<div className="flex flex-col gap-6 items-center justify-center flex-1 px-4 relative z-10">
{/* Title section */}
<div className="flex flex-col items-center justify-center pt-8 relative z-10">
<div className="text-6xl text-center font-bold uppercase w-full">transfering</div>
Expand All @@ -164,11 +165,6 @@ export default function TransferContainer() {
</div>
</div>

{/* Loading state */}
{isLoading && loadingState && (
<div className="bg-blue-50 text-blue-700 px-4 py-2 rounded-lg text-sm">{loadingState}</div>
)}

<div className="flex flex-col gap-2 mt-20">
<div className="flex gap-2 items-center justify-center w-full">
<TokenPillPopover
Expand Down Expand Up @@ -252,23 +248,38 @@ export default function TransferContainer() {
</div>

{/* Action buttons */}
{isLoading && loadingState && (
<div className="flex flex-col items-center gap-2 w-full max-w-xs">
<div className="text-sm text-gray-500">
Step {loadingStep} of {totalSteps} — {loadingState}
</div>
<div className="w-full h-1.5 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 rounded-full transition-all duration-500 ease-out"
style={{ width: `${(loadingStep / totalSteps) * 100}%` }}
/>
</div>
</div>
)}
<div className="flex gap-2 items-center justify-center w-full max-w-xs">
<button
onClick={handleAddToBatch}
disabled={isLoading || !isAmountValid || !watchedRecipient}
className="bg-main-black flex items-center justify-center px-3 py-2 rounded-[10px] disabled:opacity-50 cursor-pointer border-0 flex-1 transition-colors"
className="bg-main-black flex items-center justify-center gap-2 px-3 py-2 rounded-[10px] disabled:opacity-50 cursor-pointer border-0 flex-1 transition-colors"
>
{isLoading && <Spinner />}
<span className="font-medium xl:text-base text-xs text-center text-white tracking-[-0.16px]">
{isLoading ? "Processing..." : "Add to batch"}
</span>
</button>
<button
onClick={form.handleSubmit(handleTransfer)}
disabled={isLoading || !isAmountValid || !watchedRecipient}
className="bg-pink-350 flex items-center justify-center px-3 py-2 rounded-[10px] disabled:opacity-50 cursor-pointer border-0 flex-1 hover:bg-pink-450 transition-colors"
className="bg-pink-350 flex items-center justify-center gap-2 px-3 py-2 rounded-[10px] disabled:opacity-50 cursor-pointer border-0 flex-1 hover:bg-pink-450 transition-colors"
>
{isLoading && <Spinner />}
<span className="font-medium xl:text-base text-xs text-center tracking-[-0.16px]">
{isLoading ? "Processing..." : "Transfer now"}
{isLoading ? "Processing..." : "Submit Transfer"}
</span>
</button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ const EditStep: React.FC<EditStepProps> = ({
existingSigners,
originalThreshold,
loading,
loadingState,
onNext,
onClose,
}) => {
Expand Down Expand Up @@ -174,11 +173,6 @@ const EditStep: React.FC<EditStepProps> = ({
</button>
</div>

{/* Loading state */}
{loading && loadingState && (
<div className="px-4 py-2 bg-blue-50 text-blue-600 text-sm text-center">{loadingState}</div>
)}

{/* Content */}
<div className="flex flex-col gap-6 px-4 py-0">
{/* Account Signers Section */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

import React from "react";

const SubmittingStep = () => {
interface SubmittingStepProps {
loadingState?: string;
loadingStep?: number;
totalSteps?: number;
}

const SubmittingStep: React.FC<SubmittingStepProps> = ({ loadingState = "", loadingStep = 0, totalSteps = 4 }) => {
return (
<div className="flex flex-col items-center bg-grey-0 rounded-2xl border border-grey-200 w-[420px] py-10">
{/* Rocket animation video */}
Expand Down Expand Up @@ -30,6 +36,20 @@ const SubmittingStep = () => {
This may take a few moments. Please don&apos;t close this window.
</p>
</div>

{loadingStep > 0 && totalSteps > 0 && (
<div className="flex flex-col items-center gap-2 w-full max-w-xs mt-4">
<div className="text-sm text-gray-500">
Step {loadingStep} of {totalSteps} — {loadingState}
</div>
<div className="w-full h-1.5 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 rounded-full transition-all duration-500 ease-out"
style={{ width: `${(loadingStep / totalSteps) * 100}%` }}
/>
</div>
</div>
)}
</div>
);
};
Expand Down
6 changes: 5 additions & 1 deletion packages/nextjs/components/modals/EditAccountModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const EditAccountModal: React.FC<ModalProps> = ({ isOpen, onClose }) => {
updateThreshold,
isLoading: loading,
loadingState,
loadingStep,
totalSteps,
signers,
threshold: originalThreshold,
refetchCommitments,
Expand Down Expand Up @@ -190,7 +192,9 @@ const EditAccountModal: React.FC<ModalProps> = ({ isOpen, onClose }) => {
/>
)}

{step === "submitting" && <SubmittingStep />}
{step === "submitting" && (
<SubmittingStep loadingState={loadingState} loadingStep={loadingStep} totalSteps={totalSteps} />
)}
</ModalContainer>
);
};
Expand Down
7 changes: 7 additions & 0 deletions packages/nextjs/components/ui/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface SpinnerProps {
className?: string;
}

export function Spinner({ className = "h-4 w-4" }: SpinnerProps) {
return <div className={`animate-spin rounded-full border-2 border-current border-t-transparent ${className}`} />;
}
26 changes: 17 additions & 9 deletions packages/nextjs/hooks/app/transaction/useBatchTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -12,17 +13,24 @@ 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();
const metaMultiSigWallet = useMetaMultiSigWallet();
const { mutateAsync: createTransaction } = useCreateTransaction();
const { mutateAsync: reserveNonce } = useReserveNonce();
const { generateProof } = useGenerateProof({
onLoadingStateChange: setLoadingState,
onLoadingStateChange: setStepByLabel,
});

const proposeBatch = async (selectedBatchItems: BatchItem[]) => {
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -104,14 +111,15 @@ export const useBatchTransaction = (options?: UseBatchTransactionOptions) => {
console.error("Propose batch error:", error);
notification.error(formatErrorMessage(error, "Failed to propose batch"));
} finally {
setIsLoading(false);
setLoadingState("");
reset();
}
};

return {
proposeBatch,
isLoading,
loadingState,
loadingStep,
totalSteps,
};
};
Loading
Loading