Skip to content

Commit ee82848

Browse files
committed
Enhance wallet management page with balance fetching and improved UI components
- Introduced balance fetching for wallets, displaying loading states and balances in the wallet cards. - Added new components: WalletBalance, EmptyWalletsState, and SectionExplanation for better user experience. - Refactored CardWallet to include balance and loading state props, ensuring accurate display of wallet information. - Improved error handling and display logic for multisig wallet addresses. Files changed: - src/components/pages/homepage/wallets/index.tsx: Major updates to wallet management functionality and UI components.
1 parent 673a4a1 commit ee82848

File tree

6 files changed

+458
-16
lines changed

6 files changed

+458
-16
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import Link from "next/link";
2+
import { Button } from "@/components/ui/button";
3+
import CardUI from "@/components/common/card-content";
4+
5+
export default function EmptyWalletsState() {
6+
return (
7+
<div className="col-span-3">
8+
<CardUI
9+
title="Get Started with Multi-Signature Wallets"
10+
description=""
11+
cardClassName=""
12+
>
13+
<div className="flex flex-col gap-4">
14+
<div className="space-y-3">
15+
<p className="text-sm text-muted-foreground">
16+
Multi-signature wallets provide enhanced security for teams, DAOs,
17+
and organizations by requiring multiple approvals before any
18+
transaction can be executed.
19+
</p>
20+
21+
<div className="space-y-2">
22+
<h3 className="text-sm font-semibold">Key Features:</h3>
23+
<ul className="list-inside list-disc space-y-1 text-sm text-muted-foreground">
24+
<li>
25+
<strong>Flexible Signing Thresholds:</strong> Choose between
26+
"all", "any", or "at least N" signers required
27+
</li>
28+
<li>
29+
<strong>Team Collaboration:</strong> Perfect for treasury
30+
management and shared funds
31+
</li>
32+
<li>
33+
<strong>Governance Participation:</strong> Vote on Cardano
34+
governance proposals as a team
35+
</li>
36+
<li>
37+
<strong>Enhanced Security:</strong> Multiple signatures prevent
38+
unauthorized transactions
39+
</li>
40+
</ul>
41+
</div>
42+
</div>
43+
44+
<div className="pt-2">
45+
<Button asChild size="lg">
46+
<Link href="/wallets/new-wallet-flow/save">
47+
Create Your First Wallet
48+
</Link>
49+
</Button>
50+
</div>
51+
</div>
52+
</CardUI>
53+
</div>
54+
);
55+
}
56+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Info } from "lucide-react";
2+
3+
interface SectionExplanationProps {
4+
description: string;
5+
className?: string;
6+
}
7+
8+
export default function SectionExplanation({
9+
description,
10+
className = "",
11+
}: SectionExplanationProps) {
12+
return (
13+
<div
14+
className={`flex items-start gap-2 rounded-md bg-muted/50 p-3 text-sm text-muted-foreground ${className}`}
15+
>
16+
<Info className="mt-0.5 h-4 w-4 shrink-0" />
17+
<p>{description}</p>
18+
</div>
19+
);
20+
}
21+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import RowLabelInfo from "@/components/common/row-label-info";
2+
import { numberWithCommas } from "@/utils/strings";
3+
import WalletBalanceSkeleton from "./WalletBalanceSkeleton";
4+
5+
interface WalletBalanceProps {
6+
balance: number | null;
7+
loadingState: "idle" | "loading" | "loaded" | "error";
8+
}
9+
10+
export default function WalletBalance({
11+
balance,
12+
loadingState,
13+
}: WalletBalanceProps) {
14+
if (loadingState === "loading" || loadingState === "idle") {
15+
return <WalletBalanceSkeleton />;
16+
}
17+
18+
if (loadingState === "error") {
19+
return <RowLabelInfo label="Balance" value="—" />;
20+
}
21+
22+
// Show balance even if it's 0 (balance === 0 is valid)
23+
if (balance === null || balance === undefined) {
24+
return <RowLabelInfo label="Balance" value="—" />;
25+
}
26+
27+
return (
28+
<RowLabelInfo
29+
label="Balance"
30+
value={`₳ ${numberWithCommas(balance)}`}
31+
/>
32+
);
33+
}
34+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import RowLabelInfo from "@/components/common/row-label-info";
2+
3+
export default function WalletBalanceSkeleton() {
4+
return (
5+
<RowLabelInfo
6+
label="Balance"
7+
value={
8+
<div className="flex items-center gap-2">
9+
<div className="h-4 w-20 animate-pulse rounded bg-muted" />
10+
</div>
11+
}
12+
/>
13+
);
14+
}
15+

src/components/pages/homepage/wallets/index.tsx

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1-
import { useState } from "react";
1+
import { useState, useMemo } from "react";
22

33
import Link from "next/link";
44
import usePendingTransactions from "@/hooks/usePendingTransactions";
55
import useUserWallets from "@/hooks/useUserWallets";
6+
import useWalletBalances from "@/hooks/useWalletBalances";
67
import { Wallet } from "@/types/wallet";
78
import { getFirstAndLast } from "@/utils/strings";
89
import { api } from "@/utils/api";
910
import { useUserStore } from "@/lib/zustand/user";
11+
import { useSiteStore } from "@/lib/zustand/site";
12+
import { buildMultisigWallet } from "@/utils/common";
13+
import { addressToNetwork } from "@/utils/multisigSDK";
1014

1115
import { Button } from "@/components/ui/button";
1216
import PageHeader from "@/components/common/page-header";
1317
import CardUI from "@/components/common/card-content";
1418
import RowLabelInfo from "@/components/common/row-label-info";
1519
import SectionTitle from "@/components/common/section-title";
20+
import WalletBalance from "./WalletBalance";
21+
import EmptyWalletsState from "./EmptyWalletsState";
22+
import SectionExplanation from "./SectionExplanation";
1623

1724

1825
export default function PageWallets() {
@@ -35,6 +42,14 @@ export default function PageWallets() {
3542
},
3643
);
3744

45+
// Filter wallets for balance fetching (only non-archived or all if showing archived)
46+
const walletsForBalance = wallets?.filter(
47+
(wallet) => showArchived || !wallet.isArchived,
48+
) as Wallet[] | undefined;
49+
50+
// Fetch balances with rate limiting
51+
const { balances, loadingStates } = useWalletBalances(walletsForBalance);
52+
3853
return (
3954
<div className="flex flex-col gap-4">
4055
<>
@@ -53,15 +68,7 @@ export default function PageWallets() {
5368
</PageHeader>
5469

5570
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
56-
{wallets && wallets.length === 0 && (
57-
<div className="col-span-3 text-center text-muted-foreground">
58-
No wallets,{" "}
59-
<Link href="/wallets/new-wallet-flow/save">
60-
<b className="cursor-pointer text-white">create one</b>
61-
</Link>
62-
?
63-
</div>
64-
)}
71+
{wallets && wallets.length === 0 && <EmptyWalletsState />}
6572
{wallets &&
6673
wallets
6774
.filter((wallet) => showArchived || !wallet.isArchived)
@@ -72,14 +79,30 @@ export default function PageWallets() {
7279
? 1
7380
: -1,
7481
)
75-
.map((wallet) => (
76-
<CardWallet key={wallet.id} wallet={wallet as Wallet} />
77-
))}
82+
.map((wallet) => {
83+
const walletBalance = balances[wallet.id] ?? null;
84+
const walletLoadingState = loadingStates[wallet.id] ?? "idle";
85+
// Debug log
86+
if (process.env.NODE_ENV === "development") {
87+
console.log(`Wallet ${wallet.id}: balance=${walletBalance}, loadingState=${walletLoadingState}`);
88+
}
89+
return (
90+
<CardWallet
91+
key={wallet.id}
92+
wallet={wallet as Wallet}
93+
balance={walletBalance}
94+
loadingState={walletLoadingState}
95+
/>
96+
);
97+
})}
7898
</div>
7999

80100
{newPendingWallets && newPendingWallets.length > 0 && (
81101
<>
82102
<SectionTitle>New Wallets to be created</SectionTitle>
103+
<SectionExplanation
104+
description="These are wallets you have initiated but not yet created on-chain. Complete the wallet creation process to deploy them."
105+
/>
83106
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
84107
{newPendingWallets
85108
.sort((a, b) => a.name.localeCompare(b.name))
@@ -99,6 +122,9 @@ export default function PageWallets() {
99122
<SectionTitle>
100123
New Wallets awaiting creation
101124
</SectionTitle>
125+
<SectionExplanation
126+
description="These are wallets you have been invited to join as a signer. You can view details and accept or decline the invitation."
127+
/>
102128
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
103129
{getUserNewWalletsNotOwner
104130
.sort((a, b) => a.name.localeCompare(b.name))
@@ -113,22 +139,50 @@ export default function PageWallets() {
113139
);
114140
}
115141

116-
function CardWallet({ wallet }: { wallet: Wallet }) {
142+
function CardWallet({
143+
wallet,
144+
balance,
145+
loadingState,
146+
}: {
147+
wallet: Wallet;
148+
balance: number | null;
149+
loadingState: "idle" | "loading" | "loaded" | "error";
150+
}) {
151+
const network = useSiteStore((state) => state.network);
117152
const { transactions: pendingTransactions } = usePendingTransactions({
118153
walletId: wallet.id,
119154
});
120155

156+
// Rebuild the multisig wallet to get the correct canonical address for display
157+
// This ensures we show the correct address even if wallet.address was built incorrectly
158+
const displayAddress = useMemo(() => {
159+
try {
160+
const walletNetwork = wallet.signersAddresses.length > 0
161+
? addressToNetwork(wallet.signersAddresses[0]!)
162+
: network;
163+
const mWallet = buildMultisigWallet(wallet, walletNetwork);
164+
if (mWallet) {
165+
return mWallet.getScript().address;
166+
}
167+
} catch (error) {
168+
console.error(`Error building wallet for display: ${wallet.id}`, error);
169+
}
170+
// Fallback to wallet.address if rebuild fails (legacy support)
171+
return wallet.address;
172+
}, [wallet, network]);
173+
121174
return (
122175
<Link href={`/wallets/${wallet.id}`}>
123176
<CardUI
124177
title={`${wallet.name}${wallet.isArchived ? " (Archived)" : ""}`}
125178
description={wallet.description}
126179
cardClassName=""
127180
>
181+
<WalletBalance balance={balance} loadingState={loadingState} />
128182
<RowLabelInfo
129183
label="Address"
130-
value={getFirstAndLast(wallet.address)}
131-
copyString={wallet.address}
184+
value={getFirstAndLast(displayAddress)}
185+
copyString={displayAddress}
132186
/>
133187
<RowLabelInfo
134188
label="DRep ID"

0 commit comments

Comments
 (0)