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
647 changes: 28 additions & 619 deletions frontend/package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions frontend/src/app/(authenticated)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ export default function DashboardPage() {
<section className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-white">Recent Activity</h2>
<Link
href="/payments"
<Link
href="/payments"
className="group flex items-center gap-1.5 text-sm text-mint hover:text-glow transition-all"
>
View all payments
Expand All @@ -138,7 +138,7 @@ export default function DashboardPage() {

<FirstApiKeyModal isOpen={isFirstKeyModalOpen} onClose={() => setIsFirstKeyModalOpen(false)} />
<FirstPaymentCelebration />
<WithdrawModal isOpen={isWithdrawOpen} onClose={() => setIsWithdrawOpen(false)} />
<WithdrawalModal isOpen={isWithdrawOpen} onClose={() => setIsWithdrawOpen(false)} />
</div>
);
}
43 changes: 30 additions & 13 deletions frontend/src/app/(public)/vrt/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { Modal } from "@/components/ui/Modal";
import CopyButton from "@/components/CopyButton";
import CheckoutQrModal from "@/components/CheckoutQrModal";
import WalletSelector from "@/components/WalletSelector";

export default function VRTPage() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isQrModalOpen, setIsQrModalOpen] = useState(false);

return (
<div className="min-h-screen bg-black p-10 text-white">
Expand Down Expand Up @@ -35,20 +38,34 @@ export default function VRTPage() {
<Input label="Disabled Input" value="Cannot edit me" disabled data-testid="vrt-input-disabled" />
</section>

<section className="mb-10 space-y-4" id="vrt-modals">
<h2 className="text-lg font-bold">Modals</h2>
<Button variant="secondary" onClick={() => setIsModalOpen(true)} data-testid="open-modal-btn">
Open VRT Modal
</Button>

<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} title="VRT Check">
<p className="mb-4">This is a modal used for Visual Regression Testing.</p>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => setIsModalOpen(false)}>Cancel</Button>
<Button variant="primary" onClick={() => setIsModalOpen(false)}>Confirm</Button>
</div>
</Modal>
<section className="mb-10 space-y-4" id="vrt-qr">
<h2 className="text-lg font-bold">QR Displays</h2>
<div className="flex gap-4">
<Button variant="secondary" onClick={() => setIsQrModalOpen(true)} data-testid="open-qr-modal-btn">
Open Checkout QR
</Button>
</div>

<div className="max-w-sm rounded-2xl border border-white/10 bg-white/5 p-6">
<h3 className="mb-4 text-sm font-bold uppercase tracking-widest text-slate-500">Inline Wallet QR</h3>
<WalletSelector networkPassphrase="Test SDF Network" onConnected={() => { }} />
</div>

<CheckoutQrModal
isOpen={isQrModalOpen}
onClose={() => setIsQrModalOpen(false)}
qrValue="web+stellar:pay?destination=GABCD...&amount=100"
paymentId="vrt-test-payment"
/>
</section>

<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} title="VRT Check">
<p className="mb-4">This is a modal used for Visual Regression Testing.</p>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => setIsModalOpen(false)}>Cancel</Button>
<Button variant="primary" onClick={() => setIsModalOpen(false)}>Confirm</Button>
</div>
</Modal>
</div>
);
}
27 changes: 17 additions & 10 deletions frontend/src/app/pay/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ function LoadingSkeleton() {
</div>
</div>
<div className="flex flex-col gap-4 p-6">
{[1,2,3].map(i => <Skeleton key={i} height={48} borderRadius={12} />)}
{[1, 2, 3].map(i => <Skeleton key={i} height={48} borderRadius={12} />)}
</div>
</div>
</div>
Expand All @@ -102,10 +102,10 @@ function LoadingSkeleton() {

function StatusPill({ status, t }: { status: string; t: ReturnType<typeof useTranslations> }) {
const map: Record<string, { label: string; dot: string; cls: string }> = {
pending: { label: t("status.pending"), dot: "bg-yellow-500", cls: "border-yellow-200 bg-yellow-50 text-yellow-700" },
pending: { label: t("status.pending"), dot: "bg-yellow-500", cls: "border-yellow-200 bg-yellow-50 text-yellow-700" },
confirmed: { label: t("status.confirmed"), dot: "bg-emerald-500", cls: "border-emerald-200 bg-emerald-50 text-emerald-700" },
completed: { label: t("status.completed"), dot: "bg-emerald-500", cls: "border-emerald-200 bg-emerald-50 text-emerald-700" },
failed: { label: t("status.failed"), dot: "bg-red-500", cls: "border-red-200 bg-red-50 text-red-700" },
failed: { label: t("status.failed"), dot: "bg-red-500", cls: "border-red-200 bg-red-50 text-red-700" },
};
const s = map[status.toLowerCase()] ?? { label: status, dot: "bg-[#6B6B6B]", cls: "border-[#E8E8E8] bg-[#F9F9F9] text-[#6B6B6B]" };
return (
Expand Down Expand Up @@ -204,7 +204,7 @@ export default function PaymentPage() {
if (["confirmed", "completed", "failed"].includes(payment.status)) return;
const es = new EventSource(`${API_URL}/api/stream/${paymentId}`);
es.addEventListener("payment.confirmed", (event) => {
try { const d = JSON.parse(event.data); setPayment((p) => p ? { ...p, status: d.status, tx_id: d.tx_id } : null); toast.success(t("paymentConfirmed")); es.close(); } catch {}
try { const d = JSON.parse(event.data); setPayment((p) => p ? { ...p, status: d.status, tx_id: d.tx_id } : null); toast.success(t("paymentConfirmed")); es.close(); } catch { }
});
es.onerror = () => es.close();
return () => es.close();
Expand All @@ -221,7 +221,7 @@ export default function PaymentPage() {
if (data.payment && data.payment.status !== payment.status) {
setPayment(data.payment);
}
} catch {}
} catch { }
}, 5000);
return () => clearInterval(id);
}, [paymentId, payment, loading]);
Expand Down Expand Up @@ -274,7 +274,7 @@ export default function PaymentPage() {
const sorted = [...supported].sort((a, b) => parseFloat(balances.find(x => x.code === b)?.balance || "0") - parseFloat(balances.find(x => x.code === a)?.balance || "0"));
setSortedSourceAssets(sorted);
if (sorted.length > 0) setSourceAsset(sorted[0]);
} catch {}
} catch { }
};
load();
}, [activeProvider, assetMetadata, walletPublicKey]);
Expand Down Expand Up @@ -327,7 +327,7 @@ export default function PaymentPage() {
}
setPayment({ ...payment, status: "completed", tx_id: result.hash });
toast.success(t("paymentSent"));
setTimeout(async () => { try { await fetch(`${API_URL}/api/verify-payment/${paymentId}`, { method: "POST" }); } catch {} }, 2000);
setTimeout(async () => { try { await fetch(`${API_URL}/api/verify-payment/${paymentId}`, { method: "POST" }); } catch { } }, 2000);
} catch {
const msg = paymentError ?? t("paymentFailed");
setActionError(msg); toast.error(msg);
Expand Down Expand Up @@ -454,8 +454,15 @@ export default function PaymentPage() {
{!isSettled && !isFailed && (
<DetailRow label={t("scanToPay")}>
<div className="flex items-center gap-4 rounded-xl border border-[#E8E8E8] bg-[#F9F9F9] p-4">
<div className="rounded-lg border border-[#E8E8E8] bg-white p-2 shrink-0">
<QRCodeSVG value={paymentIntentUri} size={72} level="M" bgColor="#ffffff" fgColor="#0A0A0A" />
<div className="rounded-lg border border-[#E8E8E8] bg-white p-2 shrink-0 transition-transform hover:scale-105 active:scale-95 duration-200">
<QRCodeSVG
value={paymentIntentUri}
size={256}
level="H"
bgColor="#ffffff"
fgColor="#0A0A0A"
style={{ width: "72px", height: "72px" }}
/>
</div>
<div className="flex flex-col gap-2">
<p className="text-xs text-[#6B6B6B]">{t("scanDescription")}</p>
Expand Down Expand Up @@ -533,7 +540,7 @@ export default function PaymentPage() {
</button>
</>
) : (
<WalletSelector networkPassphrase={process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE ?? "Test SDF Network ; September 2015"} onConnected={() => {}} />
<WalletSelector networkPassphrase={process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE ?? "Test SDF Network ; September 2015"} onConnected={() => { }} />
)}
</div>
)}
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/CheckoutQrModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ export default function CheckoutQrModal({
ref={qrWrapperRef}
className="flex items-center justify-center rounded-2xl border border-white/10 bg-white p-5 transition-all hover:border-mint/30 hover:shadow-lg hover:shadow-mint/10"
>
<QRCodeCanvas value={qrValue} size={260} level="M" includeMargin />
<QRCodeCanvas
value={qrValue}
size={512}
level="H"
includeMargin
style={{ width: "100%", height: "auto", maxWidth: "240px" }}
/>
</div>
<button
type="button"
Expand Down
27 changes: 15 additions & 12 deletions frontend/src/components/PaymentDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ export default function PaymentDetailModal({
aria-modal="true"
aria-label="Payment details"
tabIndex={-1}
className={`fixed right-0 top-0 z-50 flex h-full w-full max-w-lg flex-col bg-[#050608] shadow-2xl outline-none backdrop-blur-xl transition-transform duration-300 ease-in-out ${visible ? "translate-x-0" : "translate-x-full"
className={`dark fixed right-0 top-0 z-50 flex h-full w-full max-w-lg flex-col bg-[#050608] shadow-2xl outline-none backdrop-blur-xl transition-transform duration-300 ease-in-out ${visible ? "translate-x-0" : "translate-x-full"
}`}
>
{/* Header */}
Expand Down Expand Up @@ -548,18 +548,21 @@ export default function PaymentDetailModal({
{/* QR Code — only for unsettled payments */}
{!isSettled && !isFailed && (
<DetailRow label="Scan to Pay">
<div className="flex items-center justify-center rounded-xl border border-white/10 bg-white p-4">
<QRCodeSVG
value={payment.recipient}
size={140}
level="M"
bgColor="#ffffff"
fgColor="#000000"
/>
<div className="flex flex-col items-center gap-4 rounded-xl border border-white/10 bg-white/[0.02] p-6 transition-all duration-300 hover:border-white/20">
<div className="rounded-xl bg-white p-3 shadow-[0_0_30px_rgba(255,255,255,0.05)]">
<QRCodeSVG
value={payment.recipient}
size={512}
level="H"
bgColor="#ffffff"
fgColor="#000000"
style={{ width: "100%", height: "auto", maxWidth: "160px" }}
/>
</div>
<p className="max-w-[200px] text-center text-[10px] uppercase tracking-wider text-slate-500">
Scan with Freighter or any Stellar wallet
</p>
</div>
<p className="text-center text-xs text-slate-500">
Scan with Freighter or any Stellar wallet
</p>
</DetailRow>
)}

Expand Down
29 changes: 18 additions & 11 deletions frontend/src/components/WalletSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface WalletSelectorProps {
// Freighter icon SVG
function FreighterIcon() {
return (
<svg viewBox="0 0 32 32" className="h-5 w-5" fill="none">
<svg viewBox="0 0 32 32" className="h-5 w-5" fill="none" aria-hidden="true" role="img">
<rect width="32" height="32" rx="8" fill="#7B61FF" />
<path d="M8 16h16M16 8l8 8-8 8" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Expand All @@ -24,7 +24,7 @@ function FreighterIcon() {

function WalletConnectIcon() {
return (
<svg viewBox="0 0 32 32" className="h-5 w-5" fill="none">
<svg viewBox="0 0 32 32" className="h-5 w-5" fill="none" aria-hidden="true" role="img">
<rect width="32" height="32" rx="8" fill="#3B99FC" />
<path d="M9.5 13.5c3.6-3.5 9.4-3.5 13 0l.4.4a.4.4 0 010 .6l-1.5 1.4a.2.2 0 01-.3 0l-.6-.6c-2.5-2.4-6.5-2.4-9 0l-.6.6a.2.2 0 01-.3 0L9 14.5a.4.4 0 010-.6l.5-.4zm16 3 1.3 1.3a.4.4 0 010 .6l-6 5.8a.4.4 0 01-.6 0l-4.2-4.1a.1.1 0 00-.2 0l-4.2 4.1a.4.4 0 01-.6 0l-6-5.8a.4.4 0 010-.6L6.5 16.5a.4.4 0 01.6 0l4.2 4.1c.1.1.1.1.2 0l4.2-4.1a.4.4 0 01.6 0l4.2 4.1c.1.1.1.1.2 0l4.2-4.1a.4.4 0 01.6 0z" fill="white" />
</svg>
Expand Down Expand Up @@ -213,8 +213,8 @@ export default function WalletSelector({ networkPassphrase, onConnected }: Walle
return (
<div className="flex flex-col gap-4">
<div>
<p className="text-sm font-bold text-white">{t("chooseWallet")}</p>
<p className="mt-0.5 text-xs text-slate-400">{t("description")}</p>
<p className="text-sm font-bold text-slate-900 dark:text-white">{t("chooseWallet")}</p>
<p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">{t("description")}</p>
</div>

<div className="flex flex-col gap-3">
Expand All @@ -223,17 +223,24 @@ export default function WalletSelector({ networkPassphrase, onConnected }: Walle

{/* WalletConnect QR */}
{wcUri && (
<div className="flex flex-col items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.02] p-6" aria-live="polite">
<p className="text-xs font-bold uppercase tracking-widest text-slate-500">{t("scanTitle")}</p>
<div className="rounded-xl bg-white p-3 shadow-[0_0_20px_rgba(255,255,255,0.05)]">
<QRCodeSVG value={wcUri} size={200} level="M" fgColor="#0A0A0A" bgColor="#ffffff" />
<div className="flex flex-col items-center gap-3 rounded-2xl border border-slate-200 bg-slate-50 p-6 transition-all duration-300 hover:border-pluto-300 hover:shadow-[0_0_30px_rgba(74,111,165,0.1)] dark:border-white/10 dark:bg-white/[0.02] dark:hover:border-white/20 dark:hover:shadow-[0_0_30px_rgba(255,255,255,0.05)]" aria-live="polite">
<p className="text-xs font-bold uppercase tracking-widest text-slate-500 dark:text-slate-400">{t("scanTitle")}</p>
<div className="rounded-xl bg-white p-3 shadow-sm dark:shadow-[0_0_20px_rgba(255,255,255,0.05)]">
<QRCodeSVG
value={wcUri}
size={512}
level="H"
fgColor="#0A0A0A"
bgColor="#ffffff"
style={{ width: "100%", height: "auto", maxWidth: "200px" }}
/>
</div>
<p className="text-center text-[10px] text-slate-500">{t("scanDescription")}</p>
<p className="text-center text-[10px] text-slate-500 dark:text-slate-400">{t("scanDescription")}</p>
</div>
)}

{(wcError || connectError) && (
<div className="rounded-xl border border-red-500/30 bg-red-500/10 p-3 text-center text-sm text-red-400" role="alert" aria-live="polite">
<div className="rounded-xl border border-red-200 bg-red-50 p-3 text-center text-sm text-red-600 dark:border-red-500/30 dark:bg-red-500/10 dark:text-red-400" role="alert" aria-live="polite">
{wcError || connectError}
</div>
)}
Expand All @@ -244,7 +251,7 @@ export default function WalletSelector({ networkPassphrase, onConnected }: Walle
href="https://freighter.app"
target="_blank"
rel="noopener noreferrer"
className="text-center text-[10px] font-bold uppercase tracking-widest text-[var(--pluto-500)] transition-colors duration-150 hover:text-[var(--pluto-700)]"
className="text-center text-[10px] font-bold uppercase tracking-widest text-pluto-500 transition-colors duration-150 hover:text-pluto-700 dark:text-pluto-400 dark:hover:text-pluto-300"
>
Don&apos;t have Freighter? Install it →
</a>
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/ui/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children }
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};

if (isOpen) {
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
Expand Down Expand Up @@ -75,16 +75,16 @@ export const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children }
return (
<>
{/* Backdrop */}
<div
<div
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm transition-opacity"
onClick={onClose}
data-testid="modal-backdrop"
aria-hidden="true"
/>

{/* Modals are generally center or sliding sheet in this app. Let's make a center modal for the VRT component itself. */}
{/* However, the PaymentDetail was a right sliding sheet. We'll build a standard modal dialog. */}
<div className="fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-white/10 bg-slate-900 p-6 shadow-2xl transition-all" data-testid="modal-content">
<div className="dark fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-white/10 bg-slate-900 p-6 shadow-2xl transition-all" data-testid="modal-content">
<div className="flex items-center justify-between border-b border-white/10 pb-4 mb-4">
<p id={titleId} className="font-mono text-xs uppercase tracking-[0.3em] text-mint">{title}</p>
<button
Expand Down
Loading