Skip to content

Commit 1ebfe9e

Browse files
authored
Merge pull request #1237 from Abidoyesimze/feature/fe-enhance-interactive-loading-states-in-fiat-onramp-integration-interface
feat(frontend): enhance interactive loading states in Fiat Onramp Integration Interface
2 parents 6c2ea7c + 21cc64d commit 1ebfe9e

4 files changed

Lines changed: 480 additions & 0 deletions

File tree

frontend/src/app/(authenticated)/dashboard/page.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ import FirstApiKeyModal from "@/components/FirstApiKeyModal";
1212
import FirstPaymentCelebration from "@/components/FirstPaymentCelebration";
1313
import PaymentMetrics from "@/components/PaymentMetrics";
1414
import PaymentsTabs from "@/components/PaymentsTabs";
15+
import FiatOnrampModal from "@/components/FiatOnrampModal";
1516

1617
export default function DashboardPage() {
1718
const [isFirstKeyModalOpen, setIsFirstKeyModalOpen] = useState(false);
19+
const [isOnrampModalOpen, setIsOnrampModalOpen] = useState(false);
1820
const hydrated = useMerchantHydrated();
1921
const apiKey = useMerchantApiKey();
2022
const [loading, setLoading] = useState(true);
@@ -71,6 +73,27 @@ export default function DashboardPage() {
7173
<div className="absolute inset-0 -z-10 bg-white/20 opacity-0 transition-opacity group-hover:opacity-100" />
7274
</Link>
7375

76+
<button
77+
type="button"
78+
onClick={() => setIsOnrampModalOpen(true)}
79+
className="flex items-center gap-2.5 rounded-xl border border-white/10 bg-white/5 px-5 py-3 text-sm font-medium text-slate-300 transition-all hover:bg-white/10 hover:text-white"
80+
>
81+
<svg
82+
className="h-5 w-5"
83+
fill="none"
84+
stroke="currentColor"
85+
viewBox="0 0 24 24"
86+
>
87+
<path
88+
strokeLinecap="round"
89+
strokeLinejoin="round"
90+
strokeWidth={2}
91+
d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"
92+
/>
93+
</svg>
94+
Buy / Deposit
95+
</button>
96+
7497
<Link
7598
href="/docs"
7699
className="flex items-center gap-2.5 rounded-xl border border-white/10 bg-white/5 px-5 py-3 text-sm font-medium text-slate-300 transition-all hover:bg-white/10 hover:text-white"
@@ -193,6 +216,10 @@ export default function DashboardPage() {
193216
isOpen={isFirstKeyModalOpen}
194217
onClose={() => setIsFirstKeyModalOpen(false)}
195218
/>
219+
<FiatOnrampModal
220+
isOpen={isOnrampModalOpen}
221+
onClose={() => setIsOnrampModalOpen(false)}
222+
/>
196223
<FirstPaymentCelebration />
197224
</div>
198225
);
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import React from "react";
2+
import { describe, it, expect, vi, beforeEach } from "vitest";
3+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
4+
import "@testing-library/jest-dom/vitest";
5+
6+
import FiatOnrampModal from "./FiatOnrampModal";
7+
8+
// ─── framer-motion mock ───────────────────────────────────────────────────────
9+
// jsdom has no layout engine so framer-motion's measurement APIs fail; strip
10+
// animation-only props and render plain HTML so the DOM stays inspectable.
11+
vi.mock("framer-motion", () => {
12+
const strip = (props: Record<string, unknown>) => {
13+
const { initial: _i, animate: _a, exit: _e, transition: _t, ...rest } = props;
14+
return rest;
15+
};
16+
return {
17+
motion: {
18+
div: ({ children, ...p }: any) => <div {...strip(p)}>{children}</div>,
19+
},
20+
AnimatePresence: ({ children }: any) => <>{children}</>,
21+
};
22+
});
23+
24+
vi.mock("sonner", () => ({
25+
toast: { success: vi.fn(), error: vi.fn() },
26+
}));
27+
28+
const mockGetFreighterPublicKey = vi.fn();
29+
const mockSignWithFreighter = vi.fn();
30+
vi.mock("@/lib/freighter", () => ({
31+
getFreighterPublicKey: (...args: unknown[]) => mockGetFreighterPublicKey(...args),
32+
signWithFreighter: (...args: unknown[]) => mockSignWithFreighter(...args),
33+
}));
34+
35+
const mockGetAnchorServices = vi.fn();
36+
const mockAuthenticateWithAnchor = vi.fn();
37+
const mockInitiateDeposit = vi.fn();
38+
vi.mock("@/lib/stellar", () => ({
39+
getAnchorServices: (...args: unknown[]) => mockGetAnchorServices(...args),
40+
authenticateWithAnchor: (...args: unknown[]) => mockAuthenticateWithAnchor(...args),
41+
initiateDeposit: (...args: unknown[]) => mockInitiateDeposit(...args),
42+
}));
43+
44+
describe("FiatOnrampModal", () => {
45+
beforeEach(() => {
46+
vi.clearAllMocks();
47+
mockGetFreighterPublicKey.mockResolvedValue("GABC123PUBLICKEY");
48+
mockGetAnchorServices.mockResolvedValue({
49+
transferServer: "https://anchor.example/sep24",
50+
webAuthEndpoint: "https://anchor.example/auth",
51+
signingKey: "GSIGNINGKEY",
52+
});
53+
mockSignWithFreighter.mockResolvedValue({ signedXDR: "signed-xdr" });
54+
mockAuthenticateWithAnchor.mockResolvedValue("jwt-token");
55+
mockInitiateDeposit.mockResolvedValue("https://anchor.example/interactive/abc");
56+
});
57+
58+
it("does not render when closed", () => {
59+
render(<FiatOnrampModal isOpen={false} onClose={vi.fn()} />);
60+
expect(screen.queryByText("Buy / Deposit Funds")).not.toBeInTheDocument();
61+
});
62+
63+
it("renders the asset selection form when open", () => {
64+
render(<FiatOnrampModal isOpen onClose={vi.fn()} />);
65+
expect(screen.getByText("Buy / Deposit Funds")).toBeInTheDocument();
66+
expect(screen.getByRole("button", { name: /USDC/ })).toBeInTheDocument();
67+
expect(screen.getByRole("button", { name: /SRT/ })).toBeInTheDocument();
68+
expect(screen.getByRole("button", { name: "Continue to Anchor" })).toBeInTheDocument();
69+
});
70+
71+
it("lets the user switch the selected asset", () => {
72+
render(<FiatOnrampModal isOpen onClose={vi.fn()} />);
73+
const usdcButton = screen.getByRole("button", { name: /USDC/ });
74+
const srtButton = screen.getByRole("button", { name: /SRT/ });
75+
76+
expect(usdcButton).toHaveAttribute("aria-pressed", "true");
77+
expect(srtButton).toHaveAttribute("aria-pressed", "false");
78+
79+
fireEvent.click(srtButton);
80+
81+
expect(srtButton).toHaveAttribute("aria-pressed", "true");
82+
expect(usdcButton).toHaveAttribute("aria-pressed", "false");
83+
});
84+
85+
it("walks through connecting, auth, and generating loading states before showing the interactive iframe", async () => {
86+
render(<FiatOnrampModal isOpen onClose={vi.fn()} />);
87+
88+
fireEvent.click(screen.getByRole("button", { name: "Continue to Anchor" }));
89+
90+
await waitFor(() => {
91+
expect(mockGetFreighterPublicKey).toHaveBeenCalled();
92+
expect(mockGetAnchorServices).toHaveBeenCalledWith("testanchor.stellar.org");
93+
});
94+
95+
await waitFor(() => {
96+
expect(mockAuthenticateWithAnchor).toHaveBeenCalled();
97+
});
98+
99+
await waitFor(() => {
100+
expect(mockInitiateDeposit).toHaveBeenCalledWith(
101+
"https://anchor.example/sep24",
102+
"jwt-token",
103+
"USDC",
104+
"GABC123PUBLICKEY",
105+
undefined,
106+
);
107+
});
108+
109+
await waitFor(() => {
110+
expect(screen.getByTitle("Anchor deposit form")).toBeInTheDocument();
111+
});
112+
});
113+
114+
it("passes the entered amount through to initiateDeposit", async () => {
115+
render(<FiatOnrampModal isOpen onClose={vi.fn()} />);
116+
117+
fireEvent.change(screen.getByLabelText(/Amount/), { target: { value: "250" } });
118+
fireEvent.click(screen.getByRole("button", { name: "Continue to Anchor" }));
119+
120+
await waitFor(() => {
121+
expect(mockInitiateDeposit).toHaveBeenCalledWith(
122+
expect.any(String),
123+
expect.any(String),
124+
"USDC",
125+
"GABC123PUBLICKEY",
126+
"250",
127+
);
128+
});
129+
});
130+
131+
it("shows an inline error and returns to the select step when the wallet is unavailable", async () => {
132+
mockGetFreighterPublicKey.mockRejectedValue(new Error("Freighter is not installed"));
133+
134+
render(<FiatOnrampModal isOpen onClose={vi.fn()} />);
135+
fireEvent.click(screen.getByRole("button", { name: "Continue to Anchor" }));
136+
137+
await waitFor(() => {
138+
expect(screen.getByRole("alert")).toHaveTextContent("Freighter is not installed");
139+
});
140+
141+
expect(screen.getByRole("button", { name: "Continue to Anchor" })).toBeInTheDocument();
142+
expect(mockGetAnchorServices).not.toHaveBeenCalled();
143+
});
144+
145+
it("resets state and calls onClose when closed", () => {
146+
const onClose = vi.fn();
147+
render(<FiatOnrampModal isOpen onClose={onClose} />);
148+
149+
fireEvent.click(screen.getByTestId("modal-close"));
150+
151+
expect(onClose).toHaveBeenCalled();
152+
});
153+
});

0 commit comments

Comments
 (0)