diff --git a/frontend/messages/en.json b/frontend/messages/en.json index acf89cbe..7615e9bc 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -286,8 +286,8 @@ "networkFeeUnavailable": "Network fee unavailable right now.", "cancel": "Cancel", "confirmPayment": "Confirm Payment", - "receivedTitle": "This payment has been received.", - "receivedDescription": "The transaction was confirmed on-chain via PLUTO.", + "receivedTitle": "Payment Received", + "receivedDescription": "Your transaction was successfully confirmed and settled on the Stellar network.", "downloadReceipt": "Download Receipt", "downloadReceiptLoading": "Preparing Receipt...", "receiptDownloaded": "Receipt download started.", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fedf46e6..54b6e395 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -47,6 +47,7 @@ "devDependencies": { "@axe-core/cli": "^4.11.1", "@playwright/test": "^1.58.2", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", @@ -4558,6 +4559,9 @@ } }, "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", "version": "1.0.0-rc.17", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", @@ -5953,6 +5957,26 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", @@ -6074,6 +6098,110 @@ "license": "MIT", "peer": true }, + "node_modules/@types/canvas-confetti": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", + "integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/canvas-confetti": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", @@ -9836,6 +9964,21 @@ } } }, + "node_modules/data-urls/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/data-urls/node_modules/tr46": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", @@ -10146,6 +10289,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, + "license": "MIT" "license": "MIT", "peer": true }, @@ -12223,6 +12367,21 @@ } } }, + "node_modules/html-encoding-sniffer/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/http-link-header": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.3.tgz", @@ -13297,6 +13456,21 @@ } } }, + "node_modules/jsdom/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/jsdom/node_modules/entities": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", @@ -17324,6 +17498,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, + "license": "MIT" "license": "MIT", "peer": true }, @@ -18375,6 +18550,13 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" } }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.60.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index e7833412..2526b7d6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -53,6 +53,7 @@ "devDependencies": { "@axe-core/cli": "^4.11.1", "@playwright/test": "^1.58.2", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", diff --git a/frontend/src/app/pay/[id]/page.tsx b/frontend/src/app/pay/[id]/page.tsx index 93722fad..c8ba8110 100644 --- a/frontend/src/app/pay/[id]/page.tsx +++ b/frontend/src/app/pay/[id]/page.tsx @@ -27,6 +27,7 @@ import { motion, AnimatePresence } from "framer-motion"; import { PaymentSuccessAnimation } from "@/components/PaymentSuccessAnimation"; import { useCheckoutPresence } from "@/lib/useCheckoutPresence"; import { Modal } from "@/components/ui/Modal"; +import PaymentSuccessAnimation from "@/components/PaymentSuccessAnimation"; const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000"; const NETWORK = process.env.NEXT_PUBLIC_STELLAR_NETWORK ?? "testnet"; @@ -176,6 +177,7 @@ export default function PaymentPage() { const [sourceAsset, setSourceAsset] = useState("XLM"); const [sortedSourceAssets, setSortedSourceAssets] = useState([]); const [walletPublicKey, setWalletPublicKey] = useState(null); + const [isOptimisticSettled, setIsOptimisticSettled] = useState(false); const previousWalletPublicKey = useRef(null); const { activeProvider } = useWallet(); @@ -333,22 +335,58 @@ export default function PaymentPage() { if (!payment) return; setIsPayModalOpen(false); setActionError(null); try { + const onSigned = (txHash: string) => { + // TRIGGER OPTIMISTIC SUCCESS + setIsOptimisticSettled(true); + setPayment(p => p ? { ...p, tx_id: txHash } : null); + }; + let result: { hash: string }; if (usePathPayment && pathQuote) { - result = await processPathPayment({ recipient: payment.recipient, destAmount: pathQuote.destination_amount, destAssetCode: pathQuote.destination_asset, destAssetIssuer: pathQuote.destination_asset_issuer, sendMax: pathQuote.send_max, sendAssetCode: pathQuote.source_asset, sendAssetIssuer: pathQuote.source_asset_issuer, path: pathQuote.path, memo: payment.memo, memoType: payment.memo_type }); + result = await processPathPayment({ + recipient: payment.recipient, + destAmount: pathQuote.destination_amount, + destAssetCode: pathQuote.destination_asset, + destAssetIssuer: pathQuote.destination_asset_issuer, + sendMax: pathQuote.send_max, + sendAssetCode: pathQuote.source_asset, + sendAssetIssuer: pathQuote.source_asset_issuer, + path: pathQuote.path, + memo: payment.memo, + memoType: payment.memo_type, + onSigned, + }); } else { - result = await processPayment({ recipient: payment.recipient, amount: String(payment.amount), assetCode: payment.asset, assetIssuer: payment.asset_issuer, memo: payment.memo, memoType: payment.memo_type }); + result = await processPayment({ + recipient: payment.recipient, + amount: String(payment.amount), + assetCode: payment.asset, + assetIssuer: payment.asset_issuer, + memo: payment.memo, + memoType: payment.memo_type, + onSigned, + }); } // Optimistic update: trigger animation and local state as soon as transaction hash is available setIsOptimisticSuccess(true); setPayment({ ...payment, status: "completed", tx_id: result.hash }); + setIsOptimisticSettled(false); // No longer optimistic, it's real now toast.success(t("paymentSent")); + setTimeout(async () => { + try { + await fetch(`${API_URL}/api/verify-payment/${paymentId}`, { + method: "POST", + }); + } catch {} + }, 2000); // Verification in background void fetch(`${API_URL}/api/verify-payment/${paymentId}`, { method: "POST" }).catch(() => {}); } catch { + setIsOptimisticSettled(false); const msg = paymentError ?? t("paymentFailed"); - setActionError(msg); toast.error(msg); + setActionError(msg); + toast.error(msg); } }; @@ -382,7 +420,7 @@ export default function PaymentPage() { ); } - const isSettled = payment.status === "confirmed" || payment.status === "completed"; + const isSettled = payment.status === "confirmed" || payment.status === "completed" || isOptimisticSettled; const isFailed = payment.status === "failed"; const paymentIntentUri = buildSep7Uri(payment); const branding = resolveBranding(payment.branding_config || {}); @@ -390,6 +428,15 @@ export default function PaymentPage() { return ( <> + {isProcessing && ( +
+ +
+

{txStatus ?? t("processingFallback")}

+

{t("doNotClose")}

+
+
+ )} {isOptimisticSuccess && ( + +