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
40 changes: 23 additions & 17 deletions app/send/components/AutomaticSplitCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import {
Shield,
Info,
} from "lucide-react";
import { useClientLocale } from "@/lib/i18n/client";
import { formatCurrency } from "@/lib/utils/format-currency";

interface SplitCategoryProps {
icon: React.ElementType;
label: string;
amount: number;
amount: string;
percentage: number;
}

Expand All @@ -34,7 +36,7 @@ const SplitCategory = ({
</div>
<div className="flex items-baseline gap-2">
<span className="text-sm font-bold text-white tabular-nums">
${amount.toFixed(2)}
{amount}
</span>
<span className="text-[11px] text-gray-500 w-8 text-right">
{percentage}%
Expand All @@ -53,9 +55,12 @@ const SplitCategory = ({

interface AutomaticSplitCardProps {
amount?: number;
currency?: string;
}

export default function AutomaticSplitCard({ amount: externalAmount }: AutomaticSplitCardProps) {
export default function AutomaticSplitCard({ amount: externalAmount, currency }: AutomaticSplitCardProps) {
const locale = useClientLocale();
const resolvedCurrency = currency ?? "USD";
const [internalAmount, setInternalAmount] = useState<string>("");

const total = externalAmount !== undefined ? externalAmount : (parseFloat(internalAmount) || 0);
Expand All @@ -67,10 +72,7 @@ export default function AutomaticSplitCard({ amount: externalAmount }: Automatic
{ icon: Shield, label: "Insurance", percentage: 5 },
] as const;

const displayTotal = total.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const displayTotal = formatCurrency(total, resolvedCurrency, locale);

return (
<div className="space-y-3 max-w-sm mx-auto font-sans">
Expand Down Expand Up @@ -100,15 +102,19 @@ export default function AutomaticSplitCard({ amount: externalAmount }: Automatic

{/* Categories */}
<div className="space-y-5">
{categories.map((cat, index) => (
<SplitCategory
key={index}
icon={cat.icon}
label={cat.label}
amount={(total * cat.percentage) / 100}
percentage={cat.percentage}
/>
))}
{categories.map((cat, index) => {
const splitValue = (total * cat.percentage) / 100;

return (
<SplitCategory
key={index}
icon={cat.icon}
label={cat.label}
amount={formatCurrency(splitValue, resolvedCurrency, locale)}
percentage={cat.percentage}
/>
);
})}
</div>

{/* Divider + Total */}
Expand All @@ -118,7 +124,7 @@ export default function AutomaticSplitCard({ amount: externalAmount }: Automatic
Total Amount
</span>
<span className="text-white text-3xl font-bold tabular-nums leading-none">
${displayTotal}
{displayTotal}
</span>
</div>

Expand Down
9 changes: 7 additions & 2 deletions app/send/components/ReviewStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import { Zap, ArrowLeft, ShieldCheck, User, CreditCard } from "lucide-react";
import AutomaticSplitCard from "./AutomaticSplitCard";
import { useClientLocale } from "@/lib/i18n/client";
import { formatCurrency } from "@/lib/utils/format-currency";

interface ReviewStepProps {
recipient: string;
Expand All @@ -20,6 +22,9 @@ export default function ReviewStep({
onBack,
onEmergencyAction,
}: ReviewStepProps) {
const locale = useClientLocale();
const formattedAmount = formatCurrency(amount, currency, locale);

return (
<div className="mx-auto max-w-4xl animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex flex-col lg:flex-row gap-8">
Expand Down Expand Up @@ -53,7 +58,7 @@ export default function ReviewStep({
<div>
<p className="text-xs text-zinc-500 uppercase tracking-wider font-bold mb-1">Amount to Send</p>
<p className="text-3xl font-bold text-white">
{amount.toLocaleString()} <span className="text-red-500">{currency}</span>
{formattedAmount}
</p>
</div>
</div>
Expand Down Expand Up @@ -107,7 +112,7 @@ export default function ReviewStep({
</div>

<div className="lg:flex-[1]">
<AutomaticSplitCard amount={amount} />
<AutomaticSplitCard amount={amount} currency={currency} />
</div>
</div>
</div>
Expand Down
9 changes: 7 additions & 2 deletions app/send/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import RecipientAddressInput from "./components/RecipientAddressInput";
import AmountCurrencySection from "./components/AmountCurrencySection";
import ReviewStep from "./components/ReviewStep";
import TransactionSuccessReceipt from "@/components/TransactionSuccessReceipt";
import { useClientLocale } from "@/lib/i18n/client";
import { formatCurrency } from "@/lib/utils/format-currency";

type Step = "recipient" | "amount" | "review";

Expand All @@ -34,6 +36,8 @@ export default function SendMoney() {
setStep("review");
};

const locale = useClientLocale();

const handleConfirm = () => {
// Simulate transaction processing
const mockData = {
Expand All @@ -54,12 +58,13 @@ export default function SendMoney() {

setTransactionData(mockData);
setIsSubmitted(true);
const formattedAmount = formatCurrency(amount, currency, locale);
toast({
variant: "success",
title: "Transfer submitted",
description: `Successfully sent ${amount} ${currency} to ${mockData.recipientAddress}.`,
description: `Successfully sent ${formattedAmount} to ${mockData.recipientAddress}.`,
});
console.log(`Send ${amount} ${currency} to ${recipient}`);
console.log(`Send ${formattedAmount} to ${recipient}`);
};

return (
Expand Down
7 changes: 6 additions & 1 deletion components/CurrentMoneySplitWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
"use client";

import Link from 'next/link';
import { Settings } from 'lucide-react';
import { useClientLocale } from '@/lib/i18n/client';
import { formatCurrency } from '@/lib/utils/format-currency';

export default function CurrentMoneySplitWidget() {
const locale = useClientLocale();
const allocations = [
{ label: 'Daily Spending', amount: 600, percentage: 50 },
{ label: 'Savings', amount: 360, percentage: 30 },
Expand Down Expand Up @@ -36,7 +41,7 @@ export default function CurrentMoneySplitWidget() {
<span className="font-medium text-sm text-gray-100">{item.label}</span>
</div>
<div className="flex items-center gap-4">
<span className="font-bold text-lg text-white">${item.amount}</span>
<span className="font-bold text-lg text-white">{formatCurrency(item.amount, 'USD', locale)}</span>
<span className="text-gray-600 text-sm font-medium w-8 text-right">{item.percentage}%</span>
</div>
</div>
Expand Down
20 changes: 15 additions & 5 deletions components/TransactionSuccessReceipt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import {
TrendingUp,
FileText,
Shield,
ArrowRight,
ChevronRight,
X
} from "lucide-react";
import Link from "next/link";
import { useClientLocale } from "@/lib/i18n/client";
import { formatCurrency } from "@/lib/utils/format-currency";

interface SplitDetail {
icon: React.ElementType;
Expand Down Expand Up @@ -54,6 +55,13 @@ export default function TransactionSuccessReceipt({
onClose
}: TransactionSuccessReceiptProps) {
const [copied, setCopied] = useState(false);
const locale = useClientLocale();
const formattedAmount = formatCurrency(amount, currency, locale);
const formattedFee = formatCurrency(fee, currency, locale, {
minimumFractionDigits: 4,
maximumFractionDigits: 4,
});
const showCurrencyLabel = !formattedAmount.endsWith(` ${currency}`);

const truncate = (str: string) =>
`${str.substring(0, 6)}...${str.substring(str.length - 6)}`;
Expand Down Expand Up @@ -99,8 +107,10 @@ export default function TransactionSuccessReceipt({
<div className="bg-white/5 border border-white/5 rounded-2xl p-6 text-center mb-8">
<div className="text-sm text-gray-400 mb-1">Amount Sent</div>
<div className="flex items-baseline justify-center gap-2">
<span className="text-4xl font-bold text-white">${amount.toFixed(2)}</span>
<span className="text-lg font-medium text-gray-500">{currency}</span>
<span className="text-4xl font-bold text-white">{formattedAmount}</span>
{showCurrencyLabel && (
<span className="text-lg font-medium text-gray-500">{currency}</span>
)}
</div>
</div>

Expand All @@ -119,7 +129,7 @@ export default function TransactionSuccessReceipt({
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-gray-500 font-medium">Network Fee</span>
<span className="text-white font-medium">${fee.toFixed(4)} {currency}</span>
<span className="text-white font-medium">{formattedFee}</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-gray-500 font-medium">Transaction ID</span>
Expand Down Expand Up @@ -149,7 +159,7 @@ export default function TransactionSuccessReceipt({
<split.icon className="w-3.5 h-3.5 text-gray-400" />
<span className="text-gray-400">{split.label}</span>
</div>
<span className="text-white font-bold">${split.amount.toFixed(2)}</span>
<span className="text-white font-bold">{formatCurrency(split.amount, currency, locale)}</span>
</div>
<div className="h-1 w-full bg-white/5 rounded-full overflow-hidden">
<div
Expand Down
30 changes: 30 additions & 0 deletions lib/utils/format-currency.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { formatAmount, formatCurrency } from "./format-currency";

describe("formatCurrency", () => {
it("formats USD with en-US locale", () => {
expect(formatCurrency(1234.5, "USD", "en-US")).toBe("$1,234.50");
});

it("formats negative values consistently", () => {
expect(formatCurrency(-9876.543, "USD", "en-US")).toBe("-$9,876.54");
});

it("formats zero values correctly", () => {
expect(formatCurrency(0, "USD", "en-US")).toBe("$0.00");
});

it("falls back for unknown stablecoin codes", () => {
expect(formatCurrency(1234.5, "USDC", "en-US")).toBe("1,234.50 USDC");
});

it("formats es locale using locale-specific separators", () => {
const result = formatCurrency(1234.5, "USD", "es-ES");
expect(result).toMatch(/1[.,\u202F]?234[.,]50/);
});
});

describe("formatAmount alias", () => {
it("behaves the same as formatCurrency", () => {
expect(formatAmount(1234.5, "USD", "en-US")).toBe(formatCurrency(1234.5, "USD", "en-US"));
});
});
55 changes: 55 additions & 0 deletions lib/utils/format-currency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export type FormatCurrencyOptions = {
locale?: string;
minimumFractionDigits?: number;
maximumFractionDigits?: number;
};

const DEFAULT_MINIMUM_FRACTION_DIGITS = 2;
const DEFAULT_MAXIMUM_FRACTION_DIGITS = 2;

function formatNumber(
amount: number,
locale: string,
minimumFractionDigits: number,
maximumFractionDigits: number
) {
return new Intl.NumberFormat(locale, {
minimumFractionDigits,
maximumFractionDigits,
}).format(amount);
}

/**
* Formats a numeric amount for a locale and currency/asset code.
*
* Falls back to a plain localized number with a currency suffix when
* Intl does not recognize the currency code (for example, stablecoin codes).
*/
export function formatCurrency(
amount: number,
currency: string,
locale = "en",
options: FormatCurrencyOptions = {}
): string {
const { minimumFractionDigits = DEFAULT_MINIMUM_FRACTION_DIGITS, maximumFractionDigits = DEFAULT_MAXIMUM_FRACTION_DIGITS } = options;
const normalizedCurrency = currency?.trim();
const resolvedLocale = locale || "en";

if (!normalizedCurrency) {
return formatNumber(amount, resolvedLocale, minimumFractionDigits, maximumFractionDigits);
}

try {
return new Intl.NumberFormat(resolvedLocale, {
style: "currency",
currency: normalizedCurrency,
minimumFractionDigits,
maximumFractionDigits,
}).format(amount);
} catch {
const formattedAmount = formatNumber(amount, resolvedLocale, minimumFractionDigits, maximumFractionDigits);
return `${formattedAmount} ${normalizedCurrency}`;
}
}

export const formatAmount = formatCurrency;
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.