diff --git a/frontend/e2e/send-remittance.spec.ts b/frontend/e2e/send-remittance.spec.ts
new file mode 100644
index 00000000..a04bde37
--- /dev/null
+++ b/frontend/e2e/send-remittance.spec.ts
@@ -0,0 +1,136 @@
+import { test, expect, type Page, type Route } from "@playwright/test";
+
+const MOCK_SENDER_ADDRESS = "GCJPBXSE6WCQDCEYZW6C3YVZCSSCHC4AE72L5KWKCYL2CLLL7NH5VSCI";
+const VALID_RECIPIENT = "GADR4OJZ2A6Q7V4YHLQED7XN3YXC2B5S6T7U8A9B0C1D2E3F4G5H6J7K8L9M";
+
+function seedConnectedWallet(state: { status: string; address: string }) {
+ return {
+ state: {
+ ...state,
+ network: { chainId: 2, name: "TESTNET", isSupported: true },
+ balances: [
+ { symbol: "USDC", amount: "5000.00", usdValue: 5000 },
+ { symbol: "XLM", amount: "100.00", usdValue: 12.5 },
+ ],
+ shouldAutoReconnect: true,
+ },
+ version: 0,
+ };
+}
+
+async function setupMocks(page: Page) {
+ await page.route("**/api/user/profile", async (route: Route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ id: "user_1",
+ email: "alice@example.com",
+ walletAddress: MOCK_SENDER_ADDRESS,
+ kycVerified: true,
+ }),
+ });
+ });
+
+ await page.route("**/api/pool/stats", async (route: Route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ success: true,
+ data: {
+ totalDeposits: 1000000,
+ totalOutstanding: 450000,
+ utilizationRate: 0.45,
+ apy: 0.12,
+ activeLoansCount: 154,
+ },
+ }),
+ });
+ });
+}
+
+test.describe("Send Remittance Flow", () => {
+ test("shows connect-wallet warning when wallet is not connected", async ({ page }) => {
+ await page.addInitScript(() => {
+ window.localStorage.setItem(
+ "remitlend-wallet",
+ JSON.stringify({
+ state: { status: "disconnected", address: null },
+ version: 0,
+ }),
+ );
+ });
+ await setupMocks(page);
+ await page.goto("/en/send-remittance");
+
+ await expect(
+ page.locator("text=Please connect your Stellar wallet to send remittances"),
+ ).toBeVisible();
+ });
+
+ test("validates recipient address on submit", async ({ page }) => {
+ const walletState = JSON.stringify(
+ seedConnectedWallet({ status: "connected", address: MOCK_SENDER_ADDRESS }),
+ );
+ await page.addInitScript((stateJson: string) => {
+ window.localStorage.setItem("remitlend-wallet", stateJson);
+ }, walletState);
+ await setupMocks(page);
+ await page.goto("/en/send-remittance");
+
+ await expect(page.locator("text=Send Remittance")).toBeVisible();
+
+ await page.fill('#recipientAddress', "invalid-address");
+ await page.fill('#amount', "100");
+
+ await page.getByRole("button", { name: /Review & Send/i }).click();
+
+ await expect(
+ page.locator("text=Invalid Stellar address format"),
+ ).toBeVisible();
+ });
+
+ test("sends a remittance successfully and redirects to history", async ({ page }) => {
+ const walletState = JSON.stringify(
+ seedConnectedWallet({ status: "connected", address: MOCK_SENDER_ADDRESS }),
+ );
+ await page.addInitScript((stateJson: string) => {
+ window.localStorage.setItem("remitlend-wallet", stateJson);
+ }, walletState);
+ await setupMocks(page);
+
+ await page.route("**/api/remittances", async (route: Route) => {
+ if (route.request().method() === "POST") {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ success: true, txHash: "tx_rem_abc" }),
+ });
+ } else {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify([]),
+ });
+ }
+ });
+
+ await page.goto("/en/send-remittance");
+
+ await expect(page.locator("text=Send Remittance")).toBeVisible();
+
+ await page.fill('#recipientAddress', VALID_RECIPIENT);
+ await page.fill('#amount', "250");
+
+ await page.getByRole("button", { name: /Review & Send/i }).click();
+
+ const confirmBtn = page.getByRole("button", { name: /Confirm|Send/i }).first();
+ await expect(confirmBtn).toBeVisible({ timeout: 5000 });
+ await confirmBtn.click();
+
+ await expect(page.locator("text=Remittance sent successfully")).toBeVisible({ timeout: 10000 });
+
+ await page.waitForURL("**/remittances", { timeout: 5000 });
+ });
+});
diff --git a/frontend/src/app/[locale]/globals.css b/frontend/src/app/[locale]/globals.css
index 943e73cd..4e8ea71b 100644
--- a/frontend/src/app/[locale]/globals.css
+++ b/frontend/src/app/[locale]/globals.css
@@ -6,7 +6,7 @@
/* WCAG AA Compliant: #171717 on #ffffff (15.9:1) */
--background: #ffffff;
--foreground: #171717;
- --focus-ring: #2563eb;
+ --focus-ring: #4f46e5;
}
@theme inline {
diff --git a/frontend/src/app/[locale]/repay/[loanId]/page.tsx b/frontend/src/app/[locale]/repay/[loanId]/page.tsx
index e53210e1..ffb21f5a 100644
--- a/frontend/src/app/[locale]/repay/[loanId]/page.tsx
+++ b/frontend/src/app/[locale]/repay/[loanId]/page.tsx
@@ -21,7 +21,6 @@ import {
import { useContractToast } from "../../../hooks/useContractToast";
import { TransactionPreviewModal } from "../../../components/transaction/TransactionPreviewModal";
import { useTransactionPreview } from "../../../hooks/useTransactionPreview";
-import { buildUnsignedRepaymentXdr } from "../../../utils/soroban";
import {
buildAmountHelperText,
getPrecisionError,
@@ -86,6 +85,7 @@ export default function RepayLoanPage() {
throw new Error("Contract configuration missing");
}
+ const { buildUnsignedRepaymentXdr } = await import("../../../utils/soroban");
const xdr = await buildUnsignedRepaymentXdr({
borrower: walletAddress,
loanId,
diff --git a/frontend/src/app/components/global_ui/Button.test.tsx b/frontend/src/app/components/global_ui/Button.test.tsx
deleted file mode 100644
index ad0c5e25..00000000
--- a/frontend/src/app/components/global_ui/Button.test.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { render, screen, fireEvent } from "@testing-library/react";
-import Button from "./Button";
-
-describe("Button Component", () => {
- it("renders button text", () => {
- render();
- expect(screen.getByText("Click Me")).toBeInTheDocument();
- });
-
- it("calls onClick when clicked", () => {
- const handleClick = jest.fn();
- render();
-
- fireEvent.click(screen.getByText("Click"));
- expect(handleClick).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/frontend/src/app/components/global_ui/Button.tsx b/frontend/src/app/components/global_ui/Button.tsx
deleted file mode 100644
index 54f3b44a..00000000
--- a/frontend/src/app/components/global_ui/Button.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from "react";
-
-interface ButtonProps {
- children: React.ReactNode;
- onClick?: () => void;
-}
-
-const Button: React.FC = ({ children, onClick }) => {
- return (
-
- );
-};
-
-export default Button;
diff --git a/frontend/src/app/components/ui/Button.tsx b/frontend/src/app/components/ui/Button.tsx
index cb0a39c2..2fa734a2 100644
--- a/frontend/src/app/components/ui/Button.tsx
+++ b/frontend/src/app/components/ui/Button.tsx
@@ -30,7 +30,7 @@ const Button = React.forwardRef(
const Component = "button";
const variants = {
- primary: "bg-blue-600 text-white hover:bg-blue-700 shadow-sm",
+ primary: "bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm",
secondary:
"bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700",
outline:
@@ -50,7 +50,7 @@ const Button = React.forwardRef(
return (
(
aria-invalid={error ? true : undefined}
aria-describedby={error ? errorId : helperText ? helperId : undefined}
className={cn(
- "flex h-10 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:bg-zinc-950 dark:ring-offset-zinc-950 dark:placeholder:text-zinc-500 dark:text-zinc-100",
+ "flex h-10 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:bg-zinc-950 dark:ring-offset-zinc-950 dark:placeholder:text-zinc-500 dark:text-zinc-100",
leftIcon && "pl-10",
rightIcon && "pr-10",
error && "border-red-500 focus-visible:ring-red-500",