diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 754a55be..4fa7e774 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -595,6 +595,7 @@ "good": "Good", "fair": "Fair", "poor": "Poor" + }, "darkModeTheme": { "loading": "Loading theme settings", "errorCleared": "Error cleared. Attempting to toggle theme.", @@ -685,5 +686,21 @@ "emptyAllocation": "No allocation data available yet.", "emptyAssets": "No portfolio assets available yet.", "emptyHistory": "No performance history available yet." + }, + "payment": { + "successTitle": "Payment Successful!", + "successMessage": "Your payment has been processed successfully. The transaction is now confirmed on the Stellar network.", + "amountReceived": "Amount Received", + "transactionId": "Transaction ID", + "successAnnounce": "Payment successful! {amount} {asset} has been received.", + "networkConfirmed": "Payment confirmed on the Stellar network.", + "dismissing": "Processing…", + "optimisticNote": "Transaction submitted — awaiting network confirmation.", + "successHint": "Press Escape or the close button to dismiss this message." + }, + "common": { + "close": "Close success animation", + "continue": "Continue", + "error": "Something went wrong. Please try again." } } \ No newline at end of file diff --git a/frontend/messages/es.json b/frontend/messages/es.json index f0b62327..bc70513e 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -664,5 +664,21 @@ "emptyAllocation": "Todavia no hay datos de asignacion.", "emptyAssets": "Todavia no hay activos en el portafolio.", "emptyHistory": "Todavia no hay historial de rendimiento." + }, + "payment": { + "successTitle": "¡Pago exitoso!", + "successMessage": "Tu pago se ha procesado correctamente. La transacción ya está confirmada en la red Stellar.", + "amountReceived": "Importe recibido", + "transactionId": "ID de transacción", + "successAnnounce": "¡Pago exitoso! Se han recibido {amount} {asset}.", + "networkConfirmed": "Pago confirmado en la red Stellar.", + "dismissing": "Procesando…", + "optimisticNote": "Transacción enviada, esperando confirmación de la red.", + "successHint": "Presiona Escape o el botón de cerrar para descartar este mensaje." + }, + "common": { + "close": "Cerrar animación de éxito", + "continue": "Continuar", + "error": "Algo salió mal. Inténtalo de nuevo." } } \ No newline at end of file diff --git a/frontend/messages/pt.json b/frontend/messages/pt.json index 2c04d2da..bc0b8aad 100644 --- a/frontend/messages/pt.json +++ b/frontend/messages/pt.json @@ -664,5 +664,21 @@ "emptyAllocation": "Ainda nao ha dados de alocacao.", "emptyAssets": "Ainda nao ha ativos no portafolio.", "emptyHistory": "Ainda nao ha historico de desempenho." + }, + "payment": { + "successTitle": "Pagamento realizado com sucesso!", + "successMessage": "Seu pagamento foi processado com sucesso. A transação já está confirmada na rede Stellar.", + "amountReceived": "Valor recebido", + "transactionId": "ID da transação", + "successAnnounce": "Pagamento realizado com sucesso! {amount} {asset} foram recebidos.", + "networkConfirmed": "Pagamento confirmado na rede Stellar.", + "dismissing": "Processando…", + "optimisticNote": "Transação enviada — aguardando confirmação da rede.", + "successHint": "Pressione Esc ou o botão fechar para dispensar esta mensagem." + }, + "common": { + "close": "Fechar animação de sucesso", + "continue": "Continuar", + "error": "Algo deu errado. Tente novamente." } } \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ec477514..40bef114 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,13 +20,12 @@ "@walletconnect/sign-client": "^2.23.9", "@walletconnect/types": "^2.23.8", "canvas-confetti": "^1.9.4", - "confetti": "^3.0.4", "event-source-polyfill": "^1.0.31", - "framer-motion": "^12.41.0", + "framer-motion": "^12.42.2", "marked": "^17.0.5", "motion": "^12.38.0", "next": "^14.2.5", - "next-intl": "^4.8.3", + "next-intl": "^4.13.4", "next-mdx-remote": "^6.0.0", "next-themes": "^0.4.6", "prismjs": "^1.30.0", @@ -11381,48 +11380,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, - "node_modules/confetti": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/confetti/-/confetti-3.0.5.tgz", - "integrity": "sha512-9XsKumdwX2pE53u8leYlxSHLaKbtV/LHrDZigdnQAu16rvxp5EP5gbl29Mkg9IJ0iBzfQyM7DRVnXqK847Iweg==", - "license": "MIT", - "dependencies": { - "node-fetch": "^3.3.2", - "qs": "^6.14.0", - "yayson": "^3.0.0", - "zod": "^4.1.11" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/confetti/node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/confetti/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/configstore": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/configstore/-/configstore-7.1.0.tgz", @@ -13129,29 +13086,6 @@ } } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/fflate": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", @@ -13336,18 +13270,6 @@ "node": ">= 6" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/forwarded-parse": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", @@ -13370,12 +13292,12 @@ } }, "node_modules/framer-motion": { - "version": "12.42.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.0.tgz", - "integrity": "sha512-wp7EJnfWaaEScVygKv3e20udoRz+LbtxScsuTkakAxfXmt+ReC6WyPW2nINRAGvd+hG9odwcjBLyOTPjH5pBRA==", + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", + "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", "license": "MIT", "dependencies": { - "motion-dom": "^12.42.0", + "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, @@ -14088,9 +14010,9 @@ } }, "node_modules/icu-minify": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.13.0.tgz", - "integrity": "sha512-SIFMeUHZJjzS5RvIGvybKvWoHjDm9cGVEs2EpJ8PmywOdJLWyblPm7TdPLLoUtkJtwQD7iGhl2WMptZ+N0on+w==", + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.13.4.tgz", + "integrity": "sha512-yK6HyPLGlQjqm8fTKtnBpM77z7vl7JdDBN2EXLvmgAu/b7XaOHWZb73M3ISl9ahBTehBv7RYeqqWSHfk1v2YcA==", "funding": [ { "type": "individual", @@ -14103,18 +14025,18 @@ } }, "node_modules/icu-minify/node_modules/@formatjs/icu-messageformat-parser": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.12.tgz", - "integrity": "sha512-YyzzxVgYJ8DELmmkhn0Yr0rUj0dTJFf9Jp628K3S0ysInBWxLVDOS8i3RP91cCp4DMK4WYb4cVMhWA9i4knSJg==", + "version": "3.5.15", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.15.tgz", + "integrity": "sha512-5o4grXKotAB3JqQuisLApHG43g17N+paoRTa92Jiz35Zvfemq0cVf4EDvuxyHAzmsJji7igaEowicLO/VmfJ8Q==", "license": "MIT", "dependencies": { - "@formatjs/icu-skeleton-parser": "2.1.10" + "@formatjs/icu-skeleton-parser": "2.1.11" } }, "node_modules/icu-minify/node_modules/@formatjs/icu-skeleton-parser": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.10.tgz", - "integrity": "sha512-XuSva+8ZGawk8VnD5VD6UeH8KarQ/Z022zgjHDoHmlNiAewstXuuzXc0Hk5pGFSdG+nNw5bfJKXqj1ZXHn9yUA==", + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz", + "integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==", "license": "MIT" }, "node_modules/idb": { @@ -17240,9 +17162,9 @@ } }, "node_modules/motion-dom": { - "version": "12.42.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.0.tgz", - "integrity": "sha512-M63h4n8R+quJdNhBwuLlgxM+OLYa9+I/T2pzDRboB9fLXRdbou+Gw7Zury+SkpaCyACP1JHSjHgZ1EgTkBr30w==", + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", + "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", "license": "MIT", "dependencies": { "motion-utils": "^12.39.0" @@ -17404,9 +17326,9 @@ } }, "node_modules/next-intl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.13.0.tgz", - "integrity": "sha512-OvNq2v5XLx4EkQOsAhVE9g+6zdb83XHusADCXXtIW4LILYnjEVaeINdr1lkVWKSjzwNUiMSlH5N4K0OQTRiv6A==", + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.13.4.tgz", + "integrity": "sha512-jhPAT0u0lahIK6E4gVdZAehugWCosBhLG8sV7xMzgSVoJpxHObP+Fiu+z2FfkEW0XPPtr7uEXoUlLEfhxhNMTg==", "funding": [ { "type": "individual", @@ -17418,11 +17340,11 @@ "@formatjs/intl-localematcher": "^0.8.1", "@parcel/watcher": "^2.4.1", "@swc/core": "^1.15.2", - "icu-minify": "^4.13.0", + "icu-minify": "^4.13.4", "negotiator": "^1.0.0", - "next-intl-swc-plugin-extractor": "^4.13.0", + "next-intl-swc-plugin-extractor": "^4.13.4", "po-parser": "^2.1.1", - "use-intl": "^4.13.0" + "use-intl": "^4.13.4" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", @@ -17435,9 +17357,9 @@ } }, "node_modules/next-intl-swc-plugin-extractor": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.13.0.tgz", - "integrity": "sha512-6S/fJI0KXvLCL8nhBo9P8eGaJPzmwJBTCzX0NaUIj0VyU8U89d//T+vjMLdNIXl5MlLaYH7B9MbAjb8Mvu+tqQ==", + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.13.4.tgz", + "integrity": "sha512-uN1+NMUYbG6YkO3q+rjc2bvAPX9nQ23owemvHJAyW0pRbQjVDwvNhmrV5qaak0oQc/9okbK17KLT49AoMGhVEQ==", "license": "MIT" }, "node_modules/next-intl/node_modules/@formatjs/fast-memoize": { @@ -17521,26 +17443,6 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, "node_modules/node-exports-info": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", @@ -18802,22 +18704,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -21902,9 +21788,9 @@ "license": "0BSD" }, "node_modules/use-intl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.13.0.tgz", - "integrity": "sha512-fAFDrWaASxlhXOipcOyb5VDD+YONqj6+8O8EcG/J7RBoOUF3A8YahRWLN+mBxYMrlMQB8N6Voqk5X+YC+HSL0A==", + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.13.4.tgz", + "integrity": "sha512-wRhU5zyPNgu845++EJ8ckQsi89b22QUop7NlGxNXpsnKSwEJr7WErAkdAYeVQgFTmDWsa8e2NI1e14XbWz9Ecw==", "funding": [ { "type": "individual", @@ -21915,7 +21801,7 @@ "dependencies": { "@formatjs/fast-memoize": "^3.1.0", "@schummar/icu-type-parser": "1.21.5", - "icu-minify": "^4.13.0", + "icu-minify": "^4.13.4", "intl-messageformat": "^11.1.0" }, "peerDependencies": { @@ -21923,34 +21809,34 @@ } }, "node_modules/use-intl/node_modules/@formatjs/fast-memoize": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.6.tgz", - "integrity": "sha512-H5aexk1Le7T9TPmscacZ+1pR6CTa2n1wq+HDVGXhH8TzUlQQpeXzZs91dRtmFHrbeNbjPFPfQujUqm7MHgVoXQ==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.7.tgz", + "integrity": "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==", "license": "MIT" }, "node_modules/use-intl/node_modules/@formatjs/icu-messageformat-parser": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.12.tgz", - "integrity": "sha512-YyzzxVgYJ8DELmmkhn0Yr0rUj0dTJFf9Jp628K3S0ysInBWxLVDOS8i3RP91cCp4DMK4WYb4cVMhWA9i4knSJg==", + "version": "3.5.15", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.15.tgz", + "integrity": "sha512-5o4grXKotAB3JqQuisLApHG43g17N+paoRTa92Jiz35Zvfemq0cVf4EDvuxyHAzmsJji7igaEowicLO/VmfJ8Q==", "license": "MIT", "dependencies": { - "@formatjs/icu-skeleton-parser": "2.1.10" + "@formatjs/icu-skeleton-parser": "2.1.11" } }, "node_modules/use-intl/node_modules/@formatjs/icu-skeleton-parser": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.10.tgz", - "integrity": "sha512-XuSva+8ZGawk8VnD5VD6UeH8KarQ/Z022zgjHDoHmlNiAewstXuuzXc0Hk5pGFSdG+nNw5bfJKXqj1ZXHn9yUA==", + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz", + "integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==", "license": "MIT" }, "node_modules/use-intl/node_modules/intl-messageformat": { - "version": "11.2.9", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.9.tgz", - "integrity": "sha512-cGzymZerpDhVXRKjKLgXKda9gI29TU2o88L7gwNMHp3WZVxA/0c5tX52udXbW9JklDApolvMXZG6Dhhdz5eirA==", + "version": "11.2.12", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.12.tgz", + "integrity": "sha512-KW70Xxfcvy7vV3qODfvShWkFDPMqKDAa4N+hSyVBWGNtVhTUFYaqlD/l88DaYPKiVcPP4rPQ3qnH7i5K82Mg7g==", "license": "BSD-3-Clause", "dependencies": { - "@formatjs/fast-memoize": "3.1.6", - "@formatjs/icu-messageformat-parser": "3.5.12" + "@formatjs/fast-memoize": "3.1.7", + "@formatjs/icu-messageformat-parser": "3.5.15" } }, "node_modules/use-sidecar": { @@ -22317,15 +22203,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/webdriver-bidi-protocol": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", @@ -23136,16 +23013,6 @@ "node": ">=12" } }, - "node_modules/yayson": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/yayson/-/yayson-3.0.0.tgz", - "integrity": "sha512-r3og9+qCtj7afqAQWOFzos369EaZqjJMxtkAXfn3kgjEwWcB1Z9Wm+6dcKU7fGA+MMdYUg/T7OZDA01Jp/cBcg==", - "license": "MIT", - "engines": { - "node": ">=14", - "npm": ">=6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -23158,15 +23025,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/zustand": { "version": "5.0.14", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6b4bafd2..72371b9a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,7 +26,6 @@ "@walletconnect/sign-client": "^2.23.9", "@walletconnect/types": "^2.23.8", "canvas-confetti": "^1.9.4", - "confetti": "^3.0.4", "event-source-polyfill": "^1.0.31", "framer-motion": "^12.42.2", "marked": "^17.0.5", diff --git a/frontend/src/app/pay/[id]/page.tsx b/frontend/src/app/pay/[id]/page.tsx index b065bc49..96969204 100644 --- a/frontend/src/app/pay/[id]/page.tsx +++ b/frontend/src/app/pay/[id]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { lazy, Suspense, useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import Image from "next/image"; import { useParams, useRouter } from "next/navigation"; @@ -25,15 +25,10 @@ import { QRCodeSVG } from "qrcode.react"; import { localeToLanguageTag } from "@/i18n/config"; import { useCheckoutPresence } from "@/lib/useCheckoutPresence"; import { Modal } from "@/components/ui/Modal"; - -// Lazy-load PaymentSuccessAnimation so its dependencies (framer-motion success -// variants, canvas-confetti) are excluded from the initial checkout bundle and -// only fetched the first time a payment actually succeeds. (#1179) -const PaymentSuccessAnimation = lazy(() => - import("@/components/PaymentSuccessAnimation").then((m) => ({ - default: m.PaymentSuccessAnimation, - })) -); +// PaymentSuccessAnimation is a Server Component boundary that code-splits its +// own client bundle (framer-motion, canvas-confetti) via next/dynamic and +// renders PaymentSuccessSkeleton while it loads. (#1179) +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"; @@ -402,16 +397,14 @@ export default function PaymentPage() { return ( <> {hasSucceededOnce && ( - - setIsOptimisticSuccess(false)} - amount={payment?.amount != null ? String(payment.amount) : undefined} - asset={payment?.asset} - txId={payment?.tx_id ?? undefined} - isOptimistic={payment?.status !== "confirmed" && payment?.status !== "completed"} - /> - + setIsOptimisticSuccess(false)} + amount={payment?.amount != null ? String(payment.amount) : undefined} + asset={payment?.asset} + txId={payment?.tx_id ?? undefined} + isOptimistic={payment?.status !== "confirmed" && payment?.status !== "completed"} + /> )} diff --git a/frontend/src/components/PaymentSuccessAnimation.test.tsx b/frontend/src/components/PaymentSuccessAnimation.test.tsx index 6688bf62..7f6cd72a 100644 --- a/frontend/src/components/PaymentSuccessAnimation.test.tsx +++ b/frontend/src/components/PaymentSuccessAnimation.test.tsx @@ -4,7 +4,7 @@ import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom/vitest"; import React from "react"; import confetti from "canvas-confetti"; -import { PaymentSuccessAnimation } from "./PaymentSuccessAnimation"; +import { PaymentSuccessAnimationClient as PaymentSuccessAnimation } from "./PaymentSuccessAnimationClient"; vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key, @@ -14,11 +14,7 @@ vi.mock("canvas-confetti", () => ({ default: vi.fn(), })); -// ── framer-motion mock ──────────────────────────────────────────────────────── -// Hoisted so useReducedMotion can be controlled per-test (#980) -const { mockUseReducedMotion } = vi.hoisted(() => ({ - mockUseReducedMotion: vi.fn(() => false as boolean | null), -})); +// ── motion/react mock ──────────────────────────────────────────────────────── const MOTION_PROPS = [ "initial", "animate", "exit", "transition", "variants", "viewport", @@ -35,7 +31,7 @@ function mkMotion(tag: string) { }); } -vi.mock("framer-motion", () => ({ +vi.mock("motion/react", () => ({ motion: { div: mkMotion("div"), button: mkMotion("button"), @@ -44,16 +40,31 @@ vi.mock("framer-motion", () => ({ path: mkMotion("path"), }, AnimatePresence: ({ children }: { children?: React.ReactNode }) => <>{children}, - useReducedMotion: mockUseReducedMotion, })); +// The component reads the OS "prefers reduced motion" setting via +// window.matchMedia (not a framer-motion/motion hook), so tests drive it +// through matchMedia directly (#980). +function setPrefersReducedMotion(matches: boolean) { + window.matchMedia = vi.fn().mockImplementation((query: string) => ({ + matches: matches && query === "(prefers-reduced-motion: reduce)", + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })); +} + // ───────────────────────────────────────────────────────────────────────────── describe("PaymentSuccessAnimation", () => { beforeEach(() => { vi.clearAllMocks(); vi.useRealTimers(); - mockUseReducedMotion.mockReturnValue(false); + setPrefersReducedMotion(false); }); // ── Render / visibility ──────────────────────────────────────────────────── @@ -201,7 +212,7 @@ describe("PaymentSuccessAnimation", () => { }); it("skips confetti when prefersReducedMotion is true (#980)", () => { - mockUseReducedMotion.mockReturnValue(true); + setPrefersReducedMotion(true); vi.useFakeTimers(); render(); act(() => { vi.advanceTimersByTime(200); }); diff --git a/frontend/src/components/PaymentSuccessAnimation.tsx b/frontend/src/components/PaymentSuccessAnimation.tsx index d8b6aad9..5b247769 100644 --- a/frontend/src/components/PaymentSuccessAnimation.tsx +++ b/frontend/src/components/PaymentSuccessAnimation.tsx @@ -1,538 +1,35 @@ -"use client"; - -import React, { useCallback, useEffect, useRef, useReducer, useState } from "react"; -import { motion, AnimatePresence, type Variants } from "framer-motion"; -import confetti from "canvas-confetti"; -import { useTranslations } from "next-intl"; - -interface PaymentSuccessAnimationProps { - show: boolean; - onComplete?: () => void | Promise; - amount?: string; - asset?: string; - txId?: string; - /** When true, the component indicates an optimistic (pre-confirmation) success state */ - isOptimistic?: boolean; -} - -// ── Full-motion animation variants ────────────────────────────────────────── - -const containerVariants: Variants = { - hidden: { opacity: 0, scale: 0.9 }, - visible: { - opacity: 1, - scale: 1, - transition: { - duration: 0.4, - ease: [0.16, 1, 0.3, 1], - staggerChildren: 0.08, - }, - }, - exit: { - opacity: 0, - scale: 0.95, - transition: { duration: 0.25, ease: "easeIn" }, - }, -}; - -const childVariants: Variants = { - hidden: { opacity: 0, y: 12 }, - visible: { - opacity: 1, - y: 0, - transition: { duration: 0.3, ease: [0.16, 1, 0.3, 1] }, - }, -}; - -const checkVariants: Variants = { - hidden: { scale: 0, rotate: -30 }, - visible: { - scale: 1, - rotate: 0, - transition: { type: "spring", stiffness: 320, damping: 18, delay: 0.05 }, - }, -}; - -// ── Reduced-motion variants (#980) ─────────────────────────────────────────── - -const containerVariantsReduced: Variants = { - hidden: { opacity: 0 }, - visible: { opacity: 1, transition: { duration: 0.2 } }, - exit: { opacity: 0, transition: { duration: 0.15 } }, -}; - -const childVariantsReduced: Variants = { - hidden: { opacity: 0 }, - visible: { opacity: 1, transition: { duration: 0.15 } }, -}; - -const checkVariantsReduced: Variants = { - hidden: { opacity: 0 }, - visible: { opacity: 1, transition: { duration: 0.2 } }, -}; - -interface AnimationState { - isDismissing: boolean; - announcementText: string; -} - -type AnimationAction = - | { type: "RESET" } - | { type: "SET_ANNOUNCEMENT"; payload: string } - | { type: "START_DISMISS"; payload: string } - | { type: "DISMISS_ERROR"; payload: string } - | { type: "DISMISS_SUCCESS" }; - -const initialState: AnimationState = { - isDismissing: false, - announcementText: "", -}; - -function usePrefersReducedMotion() { - const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); - - useEffect(() => { - if (typeof window === "undefined" || !window.matchMedia) return; - - const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); - const updatePreference = () => setPrefersReducedMotion(mediaQuery.matches); - - updatePreference(); - mediaQuery.addEventListener("change", updatePreference); - return () => mediaQuery.removeEventListener("change", updatePreference); - }, []); - - return prefersReducedMotion; -} - -function animationReducer(state: AnimationState, action: AnimationAction): AnimationState { - switch (action.type) { - case "RESET": - return { - isDismissing: false, - announcementText: "", - }; - case "SET_ANNOUNCEMENT": - return { - ...state, - announcementText: action.payload, - }; - case "START_DISMISS": - return { - isDismissing: true, - announcementText: action.payload, - }; - case "DISMISS_ERROR": - return { - isDismissing: false, - announcementText: action.payload, - }; - case "DISMISS_SUCCESS": - return { - ...state, - isDismissing: false, - }; - default: - return state; +/** + * PaymentSuccessAnimation — Server Component boundary + * + * The animation itself (framer-motion transitions, canvas-confetti, focus + * trap, timers) is inherently interactive and lives in + * PaymentSuccessAnimationClient ("use client"). This wrapper carries no + * client directive, so it can be rendered from a Server Component without + * pulling the animation/confetti bundle into the server render. + * + * next/dynamic with ssr:false code-splits the client bundle out of the + * initial checkout chunk and streams PaymentSuccessSkeleton while it loads, + * matching the KycFormShell/KycFormSkeleton pattern used elsewhere. + */ + +import dynamic from "next/dynamic"; +import PaymentSuccessSkeleton from "@/components/PaymentSuccessSkeleton"; +import type { PaymentSuccessAnimationProps } from "@/components/PaymentSuccessAnimationClient"; + +const PaymentSuccessAnimationClient = dynamic( + () => + import("@/components/PaymentSuccessAnimationClient").then( + (m) => m.PaymentSuccessAnimationClient + ), + { + ssr: false, + loading: () => , } -} - -export const PaymentSuccessAnimation: React.FC = ({ - show, - onComplete, - amount = "0", - asset = "XLM", - txId, - isOptimistic = false, -}) => { - const t = useTranslations(); - // Respect OS-level "prefer reduced motion" setting (#980) - const prefersReducedMotion = usePrefersReducedMotion(); - const hasTriggeredConfettiRef = useRef(false); - const completeTimerRef = useRef | null>(null); - const closeButtonRef = useRef(null); - const containerRef = useRef(null); - const prevIsOptimisticRef = useRef(isOptimistic); - - const [state, dispatch] = useReducer(animationReducer, initialState); - const { isDismissing, announcementText } = state; +); - // Reset dismiss state when dialog closes - useEffect(() => { - if (!show) { - dispatch({ type: "RESET" }); - } - }, [show]); - - // Announce network confirmation when optimistic state resolves (#980) - useEffect(() => { - if (!show) return; - if (prevIsOptimisticRef.current === true && !isOptimistic) { - dispatch({ - type: "SET_ANNOUNCEMENT", - payload: t("payment.networkConfirmed") || "Payment confirmed on the Stellar network.", - }); - } - prevIsOptimisticRef.current = isOptimistic; - }, [isOptimistic, show, t]); - - // Optimistic dismiss handler — UI responds immediately, rolls back on error (#981) - const handleDismiss = useCallback(async () => { - if (isDismissing) return; - dispatch({ - type: "START_DISMISS", - payload: t("payment.dismissing") || "Processing…", - }); - try { - await onComplete?.(); - } catch (err) { - const msg = - err instanceof Error - ? err.message - : t("common.error") || "Something went wrong. Please try again."; - dispatch({ type: "DISMISS_ERROR", payload: msg }); - } - }, [isDismissing, onComplete, t]); - - // Confetti + auto-dismiss timer - useEffect(() => { - if (!show) { - hasTriggeredConfettiRef.current = false; - if (completeTimerRef.current) { - clearTimeout(completeTimerRef.current); - completeTimerRef.current = null; - } - return; - } - - const focusTimer = setTimeout(() => { - closeButtonRef.current?.focus(); - }, 100); - - if (!hasTriggeredConfettiRef.current) { - hasTriggeredConfettiRef.current = true; - // Skip confetti for users who prefer reduced motion (#980) - if (!prefersReducedMotion) { - const accent = "#00F5D4"; - const secondary = "#6C5CE7"; - confetti({ - particleCount: 120, - spread: 78, - startVelocity: 36, - origin: { y: 0.65 }, - colors: [accent, secondary, "#00D4AA", "#ffffff"], - }); - setTimeout(() => { - confetti({ - particleCount: 40, - spread: 120, - startVelocity: 28, - origin: { x: 0.3, y: 0.7 }, - colors: [accent, secondary], - }); - confetti({ - particleCount: 40, - spread: 120, - startVelocity: 28, - origin: { x: 0.7, y: 0.7 }, - colors: [accent, secondary], - }); - }, 200); - } - } - - completeTimerRef.current = setTimeout(() => { - onComplete?.(); - }, 4000); - - return () => { - clearTimeout(focusTimer); - if (completeTimerRef.current) { - clearTimeout(completeTimerRef.current); - completeTimerRef.current = null; - } - }; - }, [show, onComplete, prefersReducedMotion]); - - // Focus trap + keyboard handling - useEffect(() => { - if (!show) return; - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - void handleDismiss(); - return; - } - if (e.key !== "Tab") return; - const container = containerRef.current; - if (!container) return; - const focusable = Array.from( - container.querySelectorAll( - 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' - ) - ).filter((el) => !el.hasAttribute("disabled")); - if (focusable.length === 0) return; - const first = focusable[0]; - const last = focusable[focusable.length - 1]; - if (e.shiftKey) { - if (document.activeElement === first) { - e.preventDefault(); - last.focus(); - } - } else { - if (document.activeElement === last) { - e.preventDefault(); - first.focus(); - } - } - }; - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }, [show, handleDismiss]); - - if (!show) return null; - - // Pick variants based on reduced-motion preference (#980) - const activeContainerVariants = prefersReducedMotion ? containerVariantsReduced : containerVariants; - const activeChildVariants = prefersReducedMotion ? childVariantsReduced : childVariants; - const activeCheckVariants = prefersReducedMotion ? checkVariantsReduced : checkVariants; - - const successAnnounce = - t("payment.successAnnounce") || - `Payment successful! ${amount} ${asset} has been received.`; - - const optimisticNote = isOptimistic - ? t("payment.optimisticNote") || "Transaction submitted — awaiting network confirmation." - : null; - - return ( - - - {/* Initial success announcement — assertive so screen readers interrupt (#980) */} -
- {successAnnounce} -
- - {/* Dynamic state announcements: confirmation, errors, pending (#980, #981) */} -
- {announcementText} -
- - - {/* Subtle radial glow */} -