diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 193fae22..3e2940aa 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -218,11 +218,21 @@ }, "walletSelector": { "chooseWallet": "Choose a wallet", + "description": "Connect your Stellar wallet to complete this payment.", + "connecting": "Connecting...", "walletConnectWaiting": "Waiting for wallet...", "noProjectId": "(no project ID)", "notInstalled": "(not installed)", + "tapToConnect": "Click to connect", "scanTitle": "Scan with your compatible wallet app", - "pairingFailed": "WalletConnect pairing failed" + "scanDescription": "Scan with Freighter mobile or any WalletConnect-compatible wallet", + "pairingFailed": "WalletConnect pairing failed", + "userRejected": "Connection request was canceled. Please approve it in your wallet to continue.", + "walletConnectUnavailable": "WalletConnect is unavailable right now. Try Freighter or enable a valid WalletConnect project ID.", + "extensionNotFound": "We couldn't find that wallet on this device. Install the extension and try again.", + "noAccountFound": "Your wallet connected, but no Stellar account was returned. Open a Stellar account in the wallet and retry.", + "walletConnectFailed": "We couldn't finish the WalletConnect handshake. Please try again.", + "connectionFailed": "We couldn't connect to that wallet right now. Please try again." }, "checkout": { "paymentRequest": "Payment Request", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 6be329bd..c4b12a5e 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -218,11 +218,21 @@ }, "walletSelector": { "chooseWallet": "Elige una billetera", + "description": "Conecta tu billetera Stellar para completar este pago.", + "connecting": "Conectando...", "walletConnectWaiting": "Esperando la billetera...", "noProjectId": "(sin project ID)", "notInstalled": "(no instalada)", + "tapToConnect": "Haz clic para conectar", "scanTitle": "Escanea con tu app de billetera Stellar", - "pairingFailed": "La vinculacion de WalletConnect fallo" + "scanDescription": "Escanea con Freighter móvil o cualquier billetera compatible con WalletConnect", + "pairingFailed": "La vinculacion de WalletConnect fallo", + "userRejected": "La solicitud de conexión fue cancelada. Apruébala en tu billetera para continuar.", + "walletConnectUnavailable": "WalletConnect no está disponible en este momento. Prueba con Freighter o habilita un WalletConnect project ID válido.", + "extensionNotFound": "No pudimos encontrar esa billetera en este dispositivo. Instala la extensión e inténtalo de nuevo.", + "noAccountFound": "La billetera se conectó, pero no devolvió ninguna cuenta Stellar. Abre una cuenta Stellar en la billetera y vuelve a intentarlo.", + "walletConnectFailed": "No pudimos completar la conexión con WalletConnect. Inténtalo de nuevo.", + "connectionFailed": "No pudimos conectar esa billetera en este momento. Inténtalo de nuevo." }, "checkout": { "paymentRequest": "Solicitud de pago", diff --git a/frontend/messages/pt.json b/frontend/messages/pt.json index b4c62eb6..9fb7811e 100644 --- a/frontend/messages/pt.json +++ b/frontend/messages/pt.json @@ -218,11 +218,21 @@ }, "walletSelector": { "chooseWallet": "Escolha uma carteira", + "description": "Conecte sua carteira Stellar para concluir este pagamento.", + "connecting": "Conectando...", "walletConnectWaiting": "Aguardando a carteira...", "noProjectId": "(sem project ID)", "notInstalled": "(nao instalada)", + "tapToConnect": "Clique para conectar", "scanTitle": "Escaneie com seu app de carteira Stellar", - "pairingFailed": "Falha no pareamento do WalletConnect" + "scanDescription": "Escaneie com o Freighter mobile ou qualquer carteira compatível com WalletConnect", + "pairingFailed": "Falha no pareamento do WalletConnect", + "userRejected": "A solicitação de conexão foi cancelada. Aprove-a na carteira para continuar.", + "walletConnectUnavailable": "O WalletConnect não está disponível agora. Tente usar o Freighter ou habilite um WalletConnect project ID válido.", + "extensionNotFound": "Não encontramos essa carteira neste dispositivo. Instale a extensão e tente novamente.", + "noAccountFound": "A carteira conectou, mas não retornou nenhuma conta Stellar. Abra uma conta Stellar na carteira e tente novamente.", + "walletConnectFailed": "Não conseguimos concluir a conexão com o WalletConnect. Tente novamente.", + "connectionFailed": "Não conseguimos conectar essa carteira agora. Tente novamente." }, "checkout": { "paymentRequest": "Solicitacao de pagamento", diff --git a/frontend/next.config.js b/frontend/next.config.js index 37fae9ca..19044089 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -5,15 +5,24 @@ const createNextIntlPlugin = require("next-intl/plugin"); const withSentryConfig = (config) => config; const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); -const withPWA = require("@ducanh2912/next-pwa").default({ - dest: "public", - disable: process.env.NODE_ENV === "development", - register: true, - skipWaiting: true, - fallbacks: { - offline: "/offline", - }, -}); +let withPWA = (config) => config; + +try { + withPWA = require("@ducanh2912/next-pwa").default({ + dest: "public", + disable: process.env.NODE_ENV === "development", + register: true, + skipWaiting: true, + fallbacks: { + offline: "/offline", + }, + }); +} catch (error) { + console.warn( + "next-pwa is unavailable; continuing without PWA support.", + error?.message ?? error, + ); +} const nextConfig = { reactStrictMode: true, diff --git a/frontend/src/app/pay/[id]/page.tsx b/frontend/src/app/pay/[id]/page.tsx index 23634b2a..23f41bc4 100644 --- a/frontend/src/app/pay/[id]/page.tsx +++ b/frontend/src/app/pay/[id]/page.tsx @@ -475,6 +475,14 @@ export default function PaymentPage() { {/* CTA */} {!isSettled && !isFailed && (
+
+

+ {t("completePayment")} +

+

+ {payment.description ?? t("paymentRequest")} +

+
{activeProvider ? ( <>

{t("connectedVia", { provider: activeProvider.name ?? "" })}

diff --git a/frontend/src/components/AnalyticsCards.tsx b/frontend/src/components/AnalyticsCards.tsx index 35695eb0..05aaf13e 100644 --- a/frontend/src/components/AnalyticsCards.tsx +++ b/frontend/src/components/AnalyticsCards.tsx @@ -84,7 +84,7 @@ export default function AnalyticsCards() { if (loading || !hydrated) { return ( -
+
{[1, 2, 3].map((i) => (
))} @@ -93,11 +93,11 @@ export default function AnalyticsCards() { } return ( -
+
{/* Total Volume */} -
-
-

+

+
+

{formatAmount(totalVolume, locale, hideCents)}

@@ -107,9 +107,9 @@ export default function AnalyticsCards() {

{/* Success Rate */} -
-
-

+

+
+

{successRate.toFixed(1)}%

@@ -119,9 +119,9 @@ export default function AnalyticsCards() {

{/* Active Intents */} -
-
-

+

+
+

{activeIntents}

diff --git a/frontend/src/components/ApiUsageChart.tsx b/frontend/src/components/ApiUsageChart.tsx index 22f1be49..e9864d02 100644 --- a/frontend/src/components/ApiUsageChart.tsx +++ b/frontend/src/components/ApiUsageChart.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import { useLocale } from "next-intl"; import { BarChart, Bar, @@ -10,6 +11,7 @@ import { Tooltip, ResponsiveContainer, } from "recharts"; +import { localeToLanguageTag } from "@/i18n/config"; import { useMerchantApiKey } from "@/lib/merchant-store"; const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000"; @@ -21,9 +23,24 @@ interface DailyUsage { export default function ApiUsageChart() { const apiKey = useMerchantApiKey(); + const locale = localeToLanguageTag(useLocale()); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [isCompact, setIsCompact] = useState(false); + + useEffect(() => { + const mediaQuery = window.matchMedia("(max-width: 640px)"); + const syncCompactState = (event?: MediaQueryList | MediaQueryListEvent) => { + setIsCompact(event?.matches ?? mediaQuery.matches); + }; + + syncCompactState(mediaQuery); + + const listener = (event: MediaQueryListEvent) => syncCompactState(event); + mediaQuery.addEventListener("change", listener); + return () => mediaQuery.removeEventListener("change", listener); + }, []); useEffect(() => { if (!apiKey) { @@ -86,10 +103,21 @@ export default function ApiUsageChart() { } const totalRequests = data.reduce((sum, day) => sum + day.requests, 0); + const formatTickLabel = (value: string) => + new Intl.DateTimeFormat(locale, { + month: "short", + day: isCompact ? undefined : "numeric", + }).format(new Date(value)); + const formatTooltipLabel = (value: string) => + new Intl.DateTimeFormat(locale, { + month: "short", + day: "numeric", + year: "numeric", + }).format(new Date(value)); return ( -

-
+
+

API Usage - Last 7 Days @@ -105,8 +133,17 @@ export default function ApiUsageChart() {

No API usage data available

) : ( - - +
+ + + value.toLocaleString(locale, { notation: "compact" })} + style={{ fontSize: isCompact ? "10px" : "12px" }} /> - [value.toLocaleString(locale), "Requests"]} + labelFormatter={formatTooltipLabel} labelStyle={{ color: "#94a3b8" }} /> - - - + + + +
)}
); diff --git a/frontend/src/components/LocaleSwitcher.tsx b/frontend/src/components/LocaleSwitcher.tsx index dfed0a43..dc76fa42 100644 --- a/frontend/src/components/LocaleSwitcher.tsx +++ b/frontend/src/components/LocaleSwitcher.tsx @@ -1,36 +1,44 @@ "use client"; import { useTransition } from "react"; -import { useRouter } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import { useLocale, useTranslations } from "next-intl"; import { + localeCookieMaxAge, localeCookieName, + type AppLocale, locales, resolveAppLocale, - type AppLocale, } from "@/i18n/config"; +import { usePathname, useRouter } from "@/i18n/navigation"; interface LocaleSwitcherProps { className?: string; } -const COOKIE_TTL_SECONDS = 60 * 60 * 24 * 365; - export default function LocaleSwitcher({ className = "", }: LocaleSwitcherProps) { const t = useTranslations("localeSwitcher"); const locale = resolveAppLocale(useLocale()); const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); const [isPending, startTransition] = useTransition(); const handleChange = (nextLocale: AppLocale) => { if (nextLocale === locale) return; - document.cookie = `${localeCookieName}=${nextLocale}; path=/; max-age=${COOKIE_TTL_SECONDS}; samesite=lax`; + document.cookie = `${localeCookieName}=${nextLocale}; path=/; max-age=${localeCookieMaxAge}; samesite=lax`; + + const query = searchParams.toString(); + const href = query ? `${pathname}?${query}` : pathname; startTransition(() => { - router.refresh(); + router.replace(href, { + locale: nextLocale, + scroll: false, + }); }); }; diff --git a/frontend/src/components/RecentPayments.tsx b/frontend/src/components/RecentPayments.tsx index f341c06a..4dceda3f 100644 --- a/frontend/src/components/RecentPayments.tsx +++ b/frontend/src/components/RecentPayments.tsx @@ -195,23 +195,25 @@ export default function RecentPayments({ showSkeleton = false }: { showSkeleton?
-
+
{[1,2,3,4].map(i => )}
-
-
- {[80,100,120,80,60].map((w,i) => )} -
- {[1,2,3,4,5].map(i => ( -
- - - - - +
+
+
+ {[80,100,120,80,60].map((w,i) => )}
- ))} + {[1,2,3,4,5].map(i => ( +
+ + + + + +
+ ))} +
); @@ -238,9 +240,9 @@ export default function RecentPayments({ showSkeleton = false }: { showSkeleton?
-

No payments yet

-

Create your first payment link to start accepting transactions.

- Create Payment Link +

{t("emptyTitle")}

+

{t("emptyDescription")}

+ {t("createPaymentLink")}
); } @@ -258,14 +260,14 @@ export default function RecentPayments({ showSkeleton = false }: { showSkeleton?
-
+
handleFilterChange("dateFrom", e.target.value)} className="rounded-xl border border-[#E8E8E8] bg-[#F9F9F9] px-3 py-2.5 text-sm text-[#0A0A0A] focus:border-[#0A0A0A] focus:outline-none transition-all [color-scheme:light]" /> @@ -275,40 +277,40 @@ export default function RecentPayments({ showSkeleton = false }: { showSkeleton? {hasActiveFilters && (
- Active: + {t("activeFilters")} {filters.search && clearFilter("search")} />} {filters.status !== "all" && clearFilter("status")} />} {filters.asset !== "all" && clearFilter("asset")} />} {filters.dateFrom && clearFilter("dateFrom")} />} {filters.dateTo && clearFilter("dateTo")} />} - +
)}
{/* Toolbar */} -
+

- {sortedPayments.length} of {totalCount} payments{hasActiveFilters ? " (filtered)" : ""} + {t("showingResults", { shown: sortedPayments.length, total: totalCount })} {hasActiveFilters ? t("filteredSuffix") : ""}

{/* Table */} -
- +
+
- - - - - - + + + + + + @@ -319,23 +321,23 @@ export default function RecentPayments({ showSkeleton = false }: { showSkeleton? onClick={() => { setSelectedPayment(payment.id); setIsSheetOpen(true); }} className={`group cursor-pointer transition-colors hover:bg-[#F9F9F9] ${flashedIds.has(payment.id) ? "bg-emerald-50" : ""}`} > - - + - - - - @@ -347,15 +349,15 @@ export default function RecentPayments({ showSkeleton = false }: { showSkeleton? {/* Pagination */} {totalCount > 0 && ( -
-
+
+
Rows:
-
+
handleSort("status")}>Status handleSort("amount")}>Amount handleSort("recipient")}>RecipientDescription handleSort("created_at")}>DateActions handleSort("status")}>{t("tableStatus")} handleSort("amount")}>{t("tableAmount")} handleSort("recipient")}>{t("tableRecipient")}Description handleSort("created_at")}>{t("tableDate")}Actions
+ {payment.amount} {payment.asset} + {payment.recipient.slice(0,8)}…{payment.recipient.slice(-6)} + {payment.description || } + {new Date(payment.created_at).toLocaleDateString(locale, { month: "short", day: "numeric", year: "numeric" })} +