Skip to content

Commit 8549e28

Browse files
committed
feat(#106): Add invoice payment breakdown modal
- Create PaymentBreakdownModal component showing fee breakdown - Display gross amount, protocol fee, net to recipients, and Stellar tx fee - Implement two-step payment flow: review → confirm - Show breakdown before payment submission - Mobile responsive design with clear fee explanation
1 parent 64a1fe5 commit 8549e28

2 files changed

Lines changed: 131 additions & 3 deletions

File tree

src/components/PayModal.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useState } from "react";
44
import { formatAmount, parseAmount } from "@stellar-split/sdk";
55
import PaymentProgress from "./PaymentProgress";
6+
import PaymentBreakdownModal from "./PaymentBreakdownModal";
67
import type { Invoice } from "@stellar-split/sdk";
78

89
interface Props {
@@ -18,6 +19,7 @@ export default function PayModal({ invoice, total, onPay, onClose }: Props) {
1819
const [email, setEmail] = useState("");
1920
const [paying, setPaying] = useState(false);
2021
const [error, setError] = useState<string | null>(null);
22+
const [showBreakdown, setShowBreakdown] = useState(false);
2123

2224
const parsed = (() => {
2325
try { return input ? parseAmount(input) : 0n; } catch { return 0n; }
@@ -27,9 +29,24 @@ export default function PayModal({ invoice, total, onPay, onClose }: Props) {
2729
const currentPct = total > 0n ? Number((invoice.funded * 100n) / total) : 0;
2830
const previewPct = total > 0n ? Math.min(100, Number((previewFunded * 100n) / total)) : 0;
2931

30-
const handleConfirm = async () => {
32+
// Mock fee breakdown (in production, call splitClient.calculateFee)
33+
const feeBreakdown = {
34+
gross: parsed,
35+
fee: parsed > 0n ? (parsed * 2n) / 100n : 0n, // 2% fee
36+
net: parsed > 0n ? parsed - (parsed * 2n) / 100n : 0n,
37+
};
38+
39+
// Mock Stellar fee (in production, call splitClient.estimateFee)
40+
const stellarFee = 100000n; // stroops
41+
42+
const handleReview = () => {
3143
if (!parsed || parsed <= 0n) return;
3244
setError(null);
45+
setShowBreakdown(true);
46+
};
47+
48+
const handleConfirm = async () => {
49+
if (!parsed || parsed <= 0n) return;
3350
setPaying(true);
3451
try {
3552
await onPay(parsed, email || undefined);
@@ -116,13 +133,24 @@ export default function PayModal({ invoice, total, onPay, onClose }: Props) {
116133

117134
<button
118135
type="button"
119-
onClick={handleConfirm}
136+
onClick={handleReview}
120137
disabled={paying || !parsed || parsed <= 0n}
121138
className="w-full px-6 py-3 rounded-lg bg-indigo-600 hover:bg-indigo-500 font-semibold transition-colors disabled:opacity-50"
122139
>
123-
{paying ? "Sending…" : "Confirm Payment"}
140+
{paying ? "Sending…" : "Review & Pay"}
124141
</button>
125142
</div>
143+
144+
{showBreakdown && (
145+
<PaymentBreakdownModal
146+
amount={parsed}
147+
feeBreakdown={feeBreakdown}
148+
stellarFee={stellarFee}
149+
onConfirm={handleConfirm}
150+
onBack={() => setShowBreakdown(false)}
151+
confirming={paying}
152+
/>
153+
)}
126154
</div>
127155
);
128156
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"use client";
2+
3+
import { formatAmount } from "@stellar-split/sdk";
4+
5+
interface FeeBreakdown {
6+
gross: bigint;
7+
fee: bigint;
8+
net: bigint;
9+
}
10+
11+
interface Props {
12+
amount: bigint;
13+
feeBreakdown: FeeBreakdown;
14+
stellarFee: bigint;
15+
onConfirm: () => void;
16+
onBack: () => void;
17+
confirming?: boolean;
18+
}
19+
20+
export default function PaymentBreakdownModal({
21+
amount,
22+
feeBreakdown,
23+
stellarFee,
24+
onConfirm,
25+
onBack,
26+
confirming = false,
27+
}: Props) {
28+
return (
29+
<div
30+
role="dialog"
31+
aria-modal="true"
32+
aria-labelledby="breakdown-modal-title"
33+
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60"
34+
onClick={(e) => e.target === e.currentTarget && onBack()}
35+
>
36+
<div className="bg-gray-900 rounded-2xl w-full max-w-md p-6 flex flex-col gap-5">
37+
<h2 id="breakdown-modal-title" className="text-lg font-semibold">Payment Breakdown</h2>
38+
39+
{/* Breakdown table */}
40+
<div className="bg-gray-800 rounded-lg overflow-hidden">
41+
<table className="w-full text-sm">
42+
<tbody>
43+
<tr className="border-b border-gray-700">
44+
<td className="px-4 py-3 text-gray-300">Gross Amount</td>
45+
<td className="px-4 py-3 text-right text-indigo-300 font-semibold">
46+
{formatAmount(feeBreakdown.gross)} USDC
47+
</td>
48+
</tr>
49+
<tr className="border-b border-gray-700">
50+
<td className="px-4 py-3 text-gray-300">Protocol Fee</td>
51+
<td className="px-4 py-3 text-right text-red-400 font-semibold">
52+
-{formatAmount(feeBreakdown.fee)} USDC
53+
</td>
54+
</tr>
55+
<tr className="border-b border-gray-700">
56+
<td className="px-4 py-3 text-gray-300">Net to Recipients</td>
57+
<td className="px-4 py-3 text-right text-green-400 font-semibold">
58+
{formatAmount(feeBreakdown.net)} USDC
59+
</td>
60+
</tr>
61+
<tr>
62+
<td className="px-4 py-3 text-gray-300">Stellar Tx Fee</td>
63+
<td className="px-4 py-3 text-right text-gray-400 text-xs">
64+
~{formatAmount(stellarFee)} USDC
65+
</td>
66+
</tr>
67+
</tbody>
68+
</table>
69+
</div>
70+
71+
{/* Fee explanation */}
72+
<div className="bg-gray-800/50 rounded-lg p-3 text-xs text-gray-400">
73+
<p>
74+
The protocol fee is deducted from your payment. Recipients receive the net amount. Stellar transaction fees are minimal and paid separately.
75+
</p>
76+
</div>
77+
78+
{/* Actions */}
79+
<div className="flex gap-3">
80+
<button
81+
type="button"
82+
onClick={onBack}
83+
disabled={confirming}
84+
className="flex-1 px-4 py-2 rounded-lg border border-gray-700 hover:border-gray-600 text-sm font-semibold transition-colors disabled:opacity-50"
85+
>
86+
Back
87+
</button>
88+
<button
89+
type="button"
90+
onClick={onConfirm}
91+
disabled={confirming}
92+
className="flex-1 px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-semibold transition-colors disabled:opacity-50"
93+
>
94+
{confirming ? "Confirming…" : "Confirm Payment"}
95+
</button>
96+
</div>
97+
</div>
98+
</div>
99+
);
100+
}

0 commit comments

Comments
 (0)