Skip to content
Open
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ yarn-error.log*

# typescript
*.tsbuildinfo
next-env.d.ts
next-env.d.ts

.pnpm-store/
.history/*
11 changes: 4 additions & 7 deletions app/(app)/mint/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ import {
import { Skeleton } from '@/components/ui/skeleton';
import { ArrowDown, ArrowUp, ArrowLeft } from 'lucide-react';
import { useApiOpts } from '@/hooks/use-api';
import { useBalance } from '@/hooks/use-balance';
import * as ratesApi from '@/lib/api/rates';
import * as mintApi from '@/lib/api/mint';
import * as burnApi from '@/lib/api/burn';
import type { RatesResponse } from '@/types/api';
import { formatAmount } from '@/lib/utils';

const BALANCE_PLACEHOLDER = "—";
const MINT_NETWORK_FEE_TEXT = "Estimated at confirmation";
const BURN_PROCESSING_FEE_TEXT = "Estimated at confirmation";

Expand All @@ -34,6 +33,7 @@ const BURN_PROCESSING_FEE_TEXT = "Estimated at confirmation";
*/
export default function MintPage() {
const opts = useApiOpts();
const { balance, loading: balanceLoading } = useBalance();
const [activeTab, setActiveTab] = useState<'mint' | 'burn' | 'rates'>('mint');
const [step, setStep] = useState<'input' | 'confirm' | 'success'>('input');
const [usdcAmount, setUsdcAmount] = useState('');
Expand All @@ -47,9 +47,6 @@ export default function MintPage() {
const [mintError, setMintError] = useState('');
const [txId, setTxId] = useState<string | null>(null);
const [executing, setExecuting] = useState(false);
const burnReceiveText = burnAmount
? `Local currency payout to ${BURN_DESTINATION_LABELS[burnDestination] ?? 'selected destination'}`
: '—';

useEffect(() => {
if (activeTab !== "rates") return;
Expand Down Expand Up @@ -153,7 +150,7 @@ export default function MintPage() {
AFK Balance
</p>
<p className="text-3xl font-bold mb-2">
AFK {formatAmount(BALANCE_PLACEHOLDER)}
{balanceLoading ? '...' : `AFK ${formatAmount(balance)}`}
</p>
<p className="text-xs opacity-75">
Native ACBU Currency
Expand Down Expand Up @@ -335,7 +332,7 @@ export default function MintPage() {
</div>
<p className="text-xs text-muted-foreground mt-2">
Available: AFK{" "}
{formatAmount(BALANCE_PLACEHOLDER)}
{balanceLoading ? '...' : formatAmount(balance)}
</p>
</div>
<Card className="border-border bg-muted p-3 mt-4">
Expand Down
16 changes: 10 additions & 6 deletions app/(app)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ import { PageContainer } from '@/components/layout/page-container';
import { SkeletonList } from '@/components/ui/skeleton-list';
import { EmptyState } from '@/components/ui/empty-state';
import { useApiOpts } from '@/hooks/use-api';
import { useBalance } from '@/hooks/use-balance';
import * as transfersApi from '@/lib/api/transfers';
import type { TransferItem } from '@/types/api';
import { cn, formatAmount } from '@/lib/utils';

const BALANCE_PLACEHOLDER = '—'; // TODO: GET /users/me/balance when available

const features = [
{ title: 'Send', description: 'Transfer money', icon: Send, href: '/send', color: 'bg-blue-100 dark:bg-blue-900/30', iconColor: 'text-blue-600 dark:text-blue-400' },
{ title: 'Mint', description: 'Create ACBU', icon: Coins, href: '/mint', color: 'bg-purple-100 dark:bg-purple-900/30', iconColor: 'text-purple-600 dark:text-purple-400' },
Expand All @@ -47,6 +46,7 @@ function formatDate(iso: string) {
*/
export default function Home() {
const [showBalance, setShowBalance] = useState(true);
const { balance, loading: balanceLoading, error: balanceError } = useBalance();
const opts = useApiOpts();
const [transfers, setTransfers] = useState<TransferItem[]>([]);
const [loading, setLoading] = useState(true);
Expand Down Expand Up @@ -87,7 +87,11 @@ export default function Home() {
<div>
<p className="text-xs font-medium text-muted-foreground mb-1">Total Balance</p>
<h2 className="text-3xl font-bold text-foreground">
{showBalance ? `ACBU ${BALANCE_PLACEHOLDER}` : '••••••'}
{!showBalance
? '••••••'
: balanceLoading
? '...'
: `ACBU ${formatAmount(balance)}`}
</h2>
</div>
<button
Expand All @@ -98,9 +102,9 @@ export default function Home() {
{showBalance ? <Eye className="w-4 h-4 text-muted-foreground" /> : <EyeOff className="w-4 h-4 text-muted-foreground" />}
</button>
</div>
{showBalance && BALANCE_PLACEHOLDER === '—' && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<span>Balance from backend when available</span>
{showBalance && balanceError && (
<div className="flex items-center gap-1 text-xs text-destructive">
<span>{balanceError}</span>
</div>
)}
</div>
Expand Down
112 changes: 45 additions & 67 deletions app/(app)/send/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import {
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsTrigger, TabsList } from "@/components/ui/tabs";
import { SkeletonList } from "@/components/ui/skeleton-list";
import { Skeleton } from "@/components/ui/skeleton";
import { Plus, Check, AlertCircle, ArrowRight } from "lucide-react";
import { useApiOpts } from "@/hooks/use-api";
import { useBalance } from "@/hooks/use-balance";
import * as transfersApi from "@/lib/api/transfers";
import * as userApi from "@/lib/api/user";
import type { TransferItem, ContactItem } from "@/types/api";
Expand All @@ -39,10 +39,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PageContainer } from "@/components/layout/page-container";

const BALANCE_PLACEHOLDER = "—";

function formatDate(iso: string) {
const d = new Date(iso);
const today = new Date();
Expand All @@ -58,6 +54,7 @@ function formatDate(iso: string) {
*/
export default function SendPage() {
const opts = useApiOpts();
const { balance, loading: balanceLoading, refetch: refetchBalance } = useBalance();
const [activeTab, setActiveTab] = useState("send");
const [showSendDialog, setShowSendDialog] = useState(false);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
Expand Down Expand Up @@ -120,6 +117,7 @@ export default function SendPage() {
opts,
);
loadTransfers();
refetchBalance();
setShowConfirmDialog(false);
setShowSendDialog(false);
setLastSentAmount(amount);
Expand Down Expand Up @@ -151,9 +149,13 @@ const getStatusColor = (status: string) => {
}
};

const exceedsBalance =
balance !== null && amount !== "" && parseFloat(amount) > balance;

const isFormValid = () =>
amount &&
parseFloat(amount) > 0 &&
!exceedsBalance &&
((useContact && selectedContact) ||
(!useContact && customRecipient.trim()));

Expand Down Expand Up @@ -200,77 +202,51 @@ const getStatusColor = (status: string) => {
</Link>
</Button>
</div>
</div>
</TabsContent>

<TabsContent value="history" className="space-y-3">
<div>
<h3 className="mb-3 text-sm font-semibold text-foreground">
Recent Transfers
</h3>
{loadingTransfers ? (
<SkeletonList count={2} itemHeight="h-14" />
) : transfers.length === 0 ? (
<div className="rounded-lg border border-border bg-card p-6 text-center">
<p className="text-sm text-muted-foreground">
No transfers yet
</p>
</div>
) : (
<div className="space-y-2">
{transfers.map((t: TransferItem) => (
<Link
key={t.transaction_id}
href={`/send/${t.transaction_id}`}
className="flex items-center justify-between rounded-lg border border-border bg-card p-4 transition-colors active:bg-muted"
>
<div className="flex-1 min-w-0">
<p className="font-medium text-foreground truncate">
Transfer
</p>
<p className="text-xs text-muted-foreground">
{formatDate(t.created_at)}
</p>
</div>
<div className="text-right">
<p className="font-semibold text-foreground">
AFK {formatAmount(t.amount_acbu)}
</p>
<Badge
variant="outline"
className={`mt-1 text-xs ${getStatusColor(t.status)}`}
>
{t.status === "completed" && (
<Check className="mr-1 h-3 w-3" />
)}
{t.status === "pending" && (
<AlertCircle className="mr-1 h-3 w-3" />
)}
{t.status.charAt(0).toUpperCase() + t.status.slice(1)}
</Badge>
</div>
</Link>
))}
</div>
</div>
</TabsContent>

<TabsContent value="history" className="space-y-3">
<div>
<h3 className="mb-3 text-sm font-semibold text-foreground">Recent Transfers</h3>
<h3 className="mb-3 text-sm font-semibold text-foreground">
Recent Transfers
</h3>
{loadingTransfers ? (
<SkeletonList count={2} itemHeight="h-14" />
) : transfers.length === 0 ? (
<div className="rounded-lg border border-border bg-card p-6 text-center"><p className="text-sm text-muted-foreground">No transfers yet</p></div>
<div className="rounded-lg border border-border bg-card p-6 text-center">
<p className="text-sm text-muted-foreground">
No transfers yet
</p>
</div>
) : (
<div className="space-y-2">
{transfers.map((t: TransferItem) => (
<Link key={t.transaction_id} href={`/send/${t.transaction_id}`} className="flex items-center justify-between rounded-lg border border-border bg-card p-4 transition-colors active:bg-muted">
<div className="flex-1 min-w-0"><p className="font-medium text-foreground truncate">Transfer</p><p className="text-xs text-muted-foreground">{formatDate(t.created_at)}</p></div>
<Link
key={t.transaction_id}
href={`/send/${t.transaction_id}`}
className="flex items-center justify-between rounded-lg border border-border bg-card p-4 transition-colors active:bg-muted"
>
<div className="flex-1 min-w-0">
<p className="font-medium text-foreground truncate">
Transfer
</p>
<p className="text-xs text-muted-foreground">
{formatDate(t.created_at)}
</p>
</div>
<div className="text-right">
<p className="font-semibold text-foreground">ACBU {formatAmount(t.amount_acbu)}</p>
<Badge variant="outline" className={`mt-1 text-xs ${getStatusColor(t.status)}`}>
{t.status === 'completed' && <Check className="mr-1 h-3 w-3" />}
{t.status === 'pending' && <AlertCircle className="mr-1 h-3 w-3" />}
<p className="font-semibold text-foreground">
ACBU {formatAmount(t.amount_acbu)}
</p>
<Badge
variant="outline"
className={`mt-1 text-xs ${getStatusColor(t.status)}`}
>
{t.status === "completed" && (
<Check className="mr-1 h-3 w-3" />
)}
{t.status === "pending" && (
<AlertCircle className="mr-1 h-3 w-3" />
)}
{t.status.charAt(0).toUpperCase() + t.status.slice(1)}
</Badge>
</div>
Expand Down Expand Up @@ -352,7 +328,9 @@ const getStatusColor = (status: string) => {
/>
</div>
{exceedsBalance && <p className="text-xs text-destructive">Insufficient balance.</p>}
<p className="text-xs text-muted-foreground">Available: ACBU {formatAmount(BALANCE_PLACEHOLDER)}</p>
<p className="text-xs text-muted-foreground">
Available: ACBU {balanceLoading ? '...' : formatAmount(balance)}
</p>
</div>

<div className="space-y-2">
Expand Down
55 changes: 55 additions & 0 deletions hooks/use-balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use client';

import { useState, useEffect, useCallback } from 'react';
import { useApiOpts } from '@/hooks/use-api';
import * as userApi from '@/lib/api/user';

interface UseBalanceReturn {
balance: number | null;
loading: boolean;
error: string;
refetch: () => void;
}

/**
* Fetches the authenticated user's ACBU wallet balance from GET /users/me/balance.
* Returns a numeric balance (null while unknown), loading flag, error string, and refetch fn.
*/
export function useBalance(): UseBalanceReturn {
const opts = useApiOpts();
const [balance, setBalance] = useState<number | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [tick, setTick] = useState(0);

const refetch = useCallback(() => setTick((t) => t + 1), []);

useEffect(() => {
let cancelled = false;
setLoading(true);
setError('');

userApi
.getBalance(opts)
.then((data) => {
if (cancelled) return;
const raw = data.balance;
const num = typeof raw === 'number' ? raw : parseFloat(raw);
setBalance(Number.isNaN(num) ? null : num);
})
.catch((e) => {
if (cancelled) return;
setBalance(null);
setError(e instanceof Error ? e.message : 'Failed to load balance');
})
.finally(() => {
if (!cancelled) setLoading(false);
});

return () => {
cancelled = true;
};
}, [opts.token, tick]);

return { balance, loading, error, refetch };
}
6 changes: 5 additions & 1 deletion lib/api/user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { get, post, patch, del } from './client';
import type { RequestOptions } from './client';
import type { UserMe, PatchMeBody, ReceiveResponse, ContactItem, GuardianItem } from '@/types/api';
import type { UserMe, PatchMeBody, ReceiveResponse, BalanceResponse, ContactItem, GuardianItem } from '@/types/api';

export async function getMe(opts?: RequestOptions): Promise<UserMe> {
return get<UserMe>('/users/me', opts);
Expand All @@ -10,6 +10,10 @@ export async function patchMe(data: PatchMeBody, opts?: RequestOptions): Promise
return patch<UserMe>('/users/me', data, opts);
}

export async function getBalance(opts?: RequestOptions): Promise<BalanceResponse> {
return get<BalanceResponse>('/users/me/balance', opts);
}

export async function getReceive(opts?: RequestOptions): Promise<ReceiveResponse> {
return get<ReceiveResponse>('/users/me/receive', opts);
}
Expand Down
Loading