Skip to content

Commit 64bd5cd

Browse files
committed
Feat/ux polish (#209)
* Improve UX of Transfer page * Improve UX of Batch page * Improve UX of EditAccountModal * Enhance transaction loading feedback in Dashboard and modals - Updated TransactionRow component to display detailed loading steps during transaction approval. - Integrated step loading functionality in useTransactionVote hook for improved user experience during transaction processes. * Changed button label from Transfer now to Submit Transfer to enhance user understanding during the transfer process.
1 parent 8d8f44c commit 64bd5cd

17 files changed

Lines changed: 265 additions & 73 deletions

packages/nextjs/components/Batch/BatchContainer.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,8 @@ export default function BatchContainer() {
292292
proposeBatch,
293293
isLoading: isProposing,
294294
loadingState,
295+
loadingStep,
296+
totalSteps,
295297
} = useBatchTransaction({
296298
onSuccess: async () => {
297299
setSelectedItems(new Set());
@@ -439,6 +441,8 @@ export default function BatchContainer() {
439441
onConfirm={handleProposeBatch}
440442
isLoading={isProposing}
441443
loadingState={loadingState}
444+
loadingStep={loadingStep}
445+
totalSteps={totalSteps}
442446
accountId={accountId}
443447
/>
444448
</div>
@@ -452,6 +456,8 @@ export default function BatchContainer() {
452456
onConfirm={handleProposeBatch}
453457
isLoading={isProposing}
454458
loadingState={loadingState}
459+
loadingStep={loadingStep}
460+
totalSteps={totalSteps}
455461
accountId={accountId}
456462
/>
457463
</div>

packages/nextjs/components/Batch/TransactionSummary.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ interface TransactionSummaryProps {
1919
isLoading?: boolean;
2020
loadingState?: string;
2121
accountId: string | null;
22+
loadingStep?: number;
23+
totalSteps?: number;
2224
}
2325

2426
const TransactionSummary: React.FC<TransactionSummaryProps> = ({
@@ -28,6 +30,8 @@ const TransactionSummary: React.FC<TransactionSummaryProps> = ({
2830
isLoading = false,
2931
loadingState = "",
3032
accountId,
33+
loadingStep = 0,
34+
totalSteps = 4,
3135
}) => {
3236
const { data: contacts = [] } = useContacts(accountId);
3337
return (
@@ -116,13 +120,26 @@ const TransactionSummary: React.FC<TransactionSummaryProps> = ({
116120

117121
{/* Confirm Button Section */}
118122
<div className="bg-grey-50 absolute bottom-0 left-0 right-0 px-5 py-4 border-t border-grey-200">
123+
{isLoading && loadingState && loadingStep > 0 && (
124+
<div className="flex flex-col items-center gap-2 w-full mb-3">
125+
<div className="text-sm text-gray-500">
126+
Step {loadingStep} of {totalSteps}{loadingState}
127+
</div>
128+
<div className="w-full h-1.5 bg-gray-200 rounded-full overflow-hidden">
129+
<div
130+
className="h-full bg-blue-500 rounded-full transition-all duration-500 ease-out"
131+
style={{ width: `${(loadingStep / totalSteps) * 100}%` }}
132+
/>
133+
</div>
134+
</div>
135+
)}
119136
<button
120137
onClick={onConfirm}
121138
disabled={isLoading || transactions.length === 0}
122139
className="flex items-center justify-center px-5 py-3 rounded-lg w-full disabled:opacity-50 bg-main-pink"
123140
>
124141
<span className="font-semibold text-sm text-center">
125-
{isLoading ? loadingState || "Processing..." : `Execute`}
142+
{isLoading ? loadingState || "Processing..." : `Submit batch`}
126143
</span>
127144
</button>
128145
</div>

packages/nextjs/components/Batch/TransactionSummaryDrawer.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ interface TransactionSummaryDrawerProps {
1919
onConfirm?: () => void;
2020
isLoading?: boolean;
2121
loadingState?: string;
22+
loadingStep?: number;
23+
totalSteps?: number;
2224
}
2325

2426
export const TransactionSummaryDrawer = memo(function TransactionSummaryDrawer({
@@ -29,6 +31,8 @@ export const TransactionSummaryDrawer = memo(function TransactionSummaryDrawer({
2931
onConfirm,
3032
isLoading = false,
3133
loadingState = "",
34+
loadingStep = 0,
35+
totalSteps = 4,
3236
}: TransactionSummaryDrawerProps) {
3337
const [isAnimating, setIsAnimating] = useState(false);
3438

@@ -68,6 +72,8 @@ export const TransactionSummaryDrawer = memo(function TransactionSummaryDrawer({
6872
accountId={accountId}
6973
isLoading={isLoading}
7074
loadingState={loadingState}
75+
loadingStep={loadingStep}
76+
totalSteps={totalSteps}
7177
className="h-full"
7278
/>
7379
</div>

packages/nextjs/components/Dashboard/TransactionRow.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,17 @@ export function TransactionRow({ tx, onSuccess }: TransactionRowProps) {
632632
// Get totalSigners realtime from wallet commitments
633633
const totalSigners = commitmentsData?.length || 0;
634634

635-
const { approve, deny, execute, isLoading: loading, loadingState } = useTransactionVote({ onSuccess });
635+
const {
636+
approve,
637+
deny,
638+
execute,
639+
isLoading: loading,
640+
loadingState,
641+
loadingStep,
642+
totalSteps,
643+
} = useTransactionVote({
644+
onSuccess,
645+
});
636646

637647
const handleApprove = async () => {
638648
await approve(tx);
@@ -718,7 +728,16 @@ export function TransactionRow({ tx, onSuccess }: TransactionRowProps) {
718728
<div className="w-full mb-2">
719729
{/* Loading State */}
720730
{loading && loadingState && (
721-
<div className="mb-1 px-4 py-2 bg-blue-50 text-blue-700 text-sm rounded-lg">{loadingState}</div>
731+
<div className="mb-1 flex">
732+
<div className="inline-flex items-center gap-2 bg-blue-50 text-blue-600 text-sm px-4 py-1 rounded-full">
733+
{loadingStep > 0 && totalSteps > 1 && (
734+
<span className="font-medium">
735+
Step {loadingStep}/{totalSteps}
736+
</span>
737+
)}
738+
<span>{loadingState}</span>
739+
</div>
740+
</div>
722741
)}
723742

724743
{/* Main Container */}

packages/nextjs/components/Transfer/TransferContainer.tsx

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ResolvedToken, parseTokenAmount } from "@polypay/shared";
66
import { parseEther } from "viem";
77
import { ContactPicker } from "~~/components/contact-book/ContactPicker";
88
import { TokenPillPopover } from "~~/components/popovers/TokenPillPopover";
9+
import { Spinner } from "~~/components/ui/Spinner";
910
import { useMetaMultiSigWallet, useTransferTransaction } from "~~/hooks";
1011
import { useCreateBatchItem } from "~~/hooks/api";
1112
import { useNetworkTokens } from "~~/hooks/app/useNetworkTokens";
@@ -69,7 +70,7 @@ export default function TransferContainer() {
6970
}
7071
}, [form]);
7172

72-
const { transfer, isLoading, loadingState } = useTransferTransaction({
73+
const { transfer, isLoading, loadingState, loadingStep, totalSteps } = useTransferTransaction({
7374
onSuccess: () => {
7475
form.reset();
7576
setSelectedContactId(null);
@@ -145,15 +146,15 @@ export default function TransferContainer() {
145146
return (
146147
<div className="overflow-hidden relative w-full h-full flex flex-col rounded-lg">
147148
{/* Background images */}
148-
<div className="absolute -top-70 flex h-[736.674px] items-center justify-center left-1/2 translate-x-[-50%] w-[780px] pointer-events-none">
149+
<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">
149150
<Image src="/transfer/top-globe.svg" alt="Top globe" className="w-full h-full" width={780} height={736} />
150151
</div>
151-
<div className="absolute -bottom-70 flex h-[736.674px] items-center justify-center left-1/2 translate-x-[-50%] w-[780px] pointer-events-none">
152+
<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">
152153
<Image src="/transfer/bottom-globe.svg" alt="Bottom globe" className="w-full h-full" width={780} height={736} />
153154
</div>
154155

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

167-
{/* Loading state */}
168-
{isLoading && loadingState && (
169-
<div className="bg-blue-50 text-blue-700 px-4 py-2 rounded-lg text-sm">{loadingState}</div>
170-
)}
171-
172168
<div className="flex flex-col gap-2 mt-20">
173169
<div className="flex gap-2 items-center justify-center w-full">
174170
<TokenPillPopover
@@ -252,23 +248,38 @@ export default function TransferContainer() {
252248
</div>
253249

254250
{/* Action buttons */}
251+
{isLoading && loadingState && (
252+
<div className="flex flex-col items-center gap-2 w-full max-w-xs">
253+
<div className="text-sm text-gray-500">
254+
Step {loadingStep} of {totalSteps}{loadingState}
255+
</div>
256+
<div className="w-full h-1.5 bg-gray-200 rounded-full overflow-hidden">
257+
<div
258+
className="h-full bg-blue-500 rounded-full transition-all duration-500 ease-out"
259+
style={{ width: `${(loadingStep / totalSteps) * 100}%` }}
260+
/>
261+
</div>
262+
</div>
263+
)}
255264
<div className="flex gap-2 items-center justify-center w-full max-w-xs">
256265
<button
257266
onClick={handleAddToBatch}
258267
disabled={isLoading || !isAmountValid || !watchedRecipient}
259-
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"
268+
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"
260269
>
270+
{isLoading && <Spinner />}
261271
<span className="font-medium xl:text-base text-xs text-center text-white tracking-[-0.16px]">
262272
{isLoading ? "Processing..." : "Add to batch"}
263273
</span>
264274
</button>
265275
<button
266276
onClick={form.handleSubmit(handleTransfer)}
267277
disabled={isLoading || !isAmountValid || !watchedRecipient}
268-
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"
278+
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"
269279
>
280+
{isLoading && <Spinner />}
270281
<span className="font-medium xl:text-base text-xs text-center tracking-[-0.16px]">
271-
{isLoading ? "Processing..." : "Transfer now"}
282+
{isLoading ? "Processing..." : "Submit Transfer"}
272283
</span>
273284
</button>
274285
</div>

packages/nextjs/components/modals/EditAccountModal/EditStep.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ const EditStep: React.FC<EditStepProps> = ({
2424
existingSigners,
2525
originalThreshold,
2626
loading,
27-
loadingState,
2827
onNext,
2928
onClose,
3029
}) => {
@@ -174,11 +173,6 @@ const EditStep: React.FC<EditStepProps> = ({
174173
</button>
175174
</div>
176175

177-
{/* Loading state */}
178-
{loading && loadingState && (
179-
<div className="px-4 py-2 bg-blue-50 text-blue-600 text-sm text-center">{loadingState}</div>
180-
)}
181-
182176
{/* Content */}
183177
<div className="flex flex-col gap-6 px-4 py-0">
184178
{/* Account Signers Section */}

packages/nextjs/components/modals/EditAccountModal/SubmittingStep.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
import React from "react";
44

5-
const SubmittingStep = () => {
5+
interface SubmittingStepProps {
6+
loadingState?: string;
7+
loadingStep?: number;
8+
totalSteps?: number;
9+
}
10+
11+
const SubmittingStep: React.FC<SubmittingStepProps> = ({ loadingState = "", loadingStep = 0, totalSteps = 4 }) => {
612
return (
713
<div className="flex flex-col items-center bg-grey-0 rounded-2xl border border-grey-200 w-[420px] py-10">
814
{/* Rocket animation video */}
@@ -30,6 +36,20 @@ const SubmittingStep = () => {
3036
This may take a few moments. Please don&apos;t close this window.
3137
</p>
3238
</div>
39+
40+
{loadingStep > 0 && totalSteps > 0 && (
41+
<div className="flex flex-col items-center gap-2 w-full max-w-xs mt-4">
42+
<div className="text-sm text-gray-500">
43+
Step {loadingStep} of {totalSteps}{loadingState}
44+
</div>
45+
<div className="w-full h-1.5 bg-gray-200 rounded-full overflow-hidden">
46+
<div
47+
className="h-full bg-blue-500 rounded-full transition-all duration-500 ease-out"
48+
style={{ width: `${(loadingStep / totalSteps) * 100}%` }}
49+
/>
50+
</div>
51+
</div>
52+
)}
3353
</div>
3454
);
3555
};

packages/nextjs/components/modals/EditAccountModal/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const EditAccountModal: React.FC<ModalProps> = ({ isOpen, onClose }) => {
2525
updateThreshold,
2626
isLoading: loading,
2727
loadingState,
28+
loadingStep,
29+
totalSteps,
2830
signers,
2931
threshold: originalThreshold,
3032
refetchCommitments,
@@ -190,7 +192,9 @@ const EditAccountModal: React.FC<ModalProps> = ({ isOpen, onClose }) => {
190192
/>
191193
)}
192194

193-
{step === "submitting" && <SubmittingStep />}
195+
{step === "submitting" && (
196+
<SubmittingStep loadingState={loadingState} loadingStep={loadingStep} totalSteps={totalSteps} />
197+
)}
194198
</ModalContainer>
195199
);
196200
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
interface SpinnerProps {
2+
className?: string;
3+
}
4+
5+
export function Spinner({ className = "h-4 w-4" }: SpinnerProps) {
6+
return <div className={`animate-spin rounded-full border-2 border-current border-t-transparent ${className}`} />;
7+
}

packages/nextjs/hooks/app/transaction/useBatchTransaction.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useWalletClient } from "wagmi";
44
import { useMetaMultiSigWallet } from "~~/hooks";
55
import { useCreateTransaction, useReserveNonce } from "~~/hooks/api";
66
import { useGenerateProof } from "~~/hooks/app/useGenerateProof";
7+
import { useStepLoading } from "~~/hooks/app/useStepLoading";
78
import { useIdentityStore } from "~~/services/store";
89
import { formatErrorMessage } from "~~/utils/formatError";
910
import { notification } from "~~/utils/scaffold-eth";
@@ -12,17 +13,24 @@ interface UseBatchTransactionOptions {
1213
onSuccess?: () => void;
1314
}
1415

16+
const BATCH_STEPS = [
17+
{ id: 1, label: "Preparing your batch..." },
18+
{ id: 2, label: "Waiting for wallet approval..." },
19+
{ id: 3, label: "Securing your transaction..." },
20+
{ id: 4, label: "Almost done, submitting..." },
21+
];
22+
1523
export const useBatchTransaction = (options?: UseBatchTransactionOptions) => {
16-
const [isLoading, setIsLoading] = useState(false);
17-
const [loadingState, setLoadingState] = useState("");
24+
const { isLoading, loadingState, loadingStep, totalSteps, startStep, setStepByLabel, reset } =
25+
useStepLoading(BATCH_STEPS);
1826

1927
const { data: walletClient } = useWalletClient();
2028
const { secret, commitment: myCommitment } = useIdentityStore();
2129
const metaMultiSigWallet = useMetaMultiSigWallet();
2230
const { mutateAsync: createTransaction } = useCreateTransaction();
2331
const { mutateAsync: reserveNonce } = useReserveNonce();
2432
const { generateProof } = useGenerateProof({
25-
onLoadingStateChange: setLoadingState,
33+
onLoadingStateChange: setStepByLabel,
2634
});
2735

2836
const proposeBatch = async (selectedBatchItems: BatchItem[]) => {
@@ -42,16 +50,15 @@ export const useBatchTransaction = (options?: UseBatchTransactionOptions) => {
4250
return;
4351
}
4452

45-
setIsLoading(true);
46-
4753
try {
4854
const selectedIds = selectedBatchItems.map(item => item.id);
4955

5056
// 1. Reserve nonce from backend
57+
startStep(1);
5158
const { nonce } = await reserveNonce(metaMultiSigWallet.address);
5259

5360
// 2. Get current threshold and commitments
54-
setLoadingState("Preparing batch transaction...");
61+
startStep(1);
5562
const currentThreshold = await metaMultiSigWallet.read.signaturesRequired();
5663

5764
// 3. Prepare batch data
@@ -79,7 +86,7 @@ export const useBatchTransaction = (options?: UseBatchTransactionOptions) => {
7986
const { proof, publicInputs, nullifier, vk } = await generateProof(txHash);
8087

8188
// 7. Submit to backend
82-
setLoadingState("Submitting to backend...");
89+
startStep(4);
8390
const result = await createTransaction({
8491
nonce,
8592
type: TxType.BATCH,
@@ -104,14 +111,15 @@ export const useBatchTransaction = (options?: UseBatchTransactionOptions) => {
104111
console.error("Propose batch error:", error);
105112
notification.error(formatErrorMessage(error, "Failed to propose batch"));
106113
} finally {
107-
setIsLoading(false);
108-
setLoadingState("");
114+
reset();
109115
}
110116
};
111117

112118
return {
113119
proposeBatch,
114120
isLoading,
115121
loadingState,
122+
loadingStep,
123+
totalSteps,
116124
};
117125
};

0 commit comments

Comments
 (0)