From 9f3cc025ee1c31a5c890841ddae82d70870c3c8b Mon Sep 17 00:00:00 2001 From: tali-creator Date: Tue, 18 Nov 2025 18:35:12 +0100 Subject: [PATCH 1/9] fixed --- .gitignore | 3 + .vscode/mcp.json | 8 + components/dashboard/quick-actions.tsx | 8 +- components/dashboard/service-flow.tsx | 6 +- components/dashboard/stats-cards.tsx | 10 +- components/dashboard/tabs/dashboard.tsx | 15 +- components/dashboard/tabs/send-funds.tsx | 4 +- components/dashboard/wallet-overview.tsx | 52 +- components/hooks/useTokenBalance.ts | 8 +- lib/api-client.ts | 10 +- next.config.ts | 34 + package-lock.json | 5460 ++++++++++++++++------ 12 files changed, 4227 insertions(+), 1391 deletions(-) create mode 100644 .vscode/mcp.json diff --git a/.gitignore b/.gitignore index 5ef6a52..dd146b5 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Sentry Config File +.env.sentry-build-plugin diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..bea3b3d --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,8 @@ +{ + "servers": { + "Sentry": { + "url": "https://mcp.sentry.dev/mcp/velo-7b/javascript-nextjs", + "type": "http" + } + } +} \ No newline at end of file diff --git a/components/dashboard/quick-actions.tsx b/components/dashboard/quick-actions.tsx index aa9117a..0c0a507 100644 --- a/components/dashboard/quick-actions.tsx +++ b/components/dashboard/quick-actions.tsx @@ -41,12 +41,12 @@ const actions = [ export function QuickActions({ setTab }: quickActionProps) { return ( - + Quick Actions - -
+ +
{actions.map((action) => { const Icon = action.icon; return ( @@ -63,7 +63,7 @@ export function QuickActions({ setTab }: quickActionProps) {
{action.title}
-
+
{action.description}
diff --git a/components/dashboard/service-flow.tsx b/components/dashboard/service-flow.tsx index 302b8d6..d77f483 100644 --- a/components/dashboard/service-flow.tsx +++ b/components/dashboard/service-flow.tsx @@ -358,9 +358,9 @@ export default function Purchase({ type }: PurchaseProps) { // console.log("current wallet", currentWalletAddress); const currentNetwork = useMemo(() => { - if (!addresses) return "mainnet"; + if (!addresses) return "testnet"; const addressInfo = addresses.find((addr) => addr.chain === selectedToken); - return addressInfo?.network || "mainnet"; + return addressInfo?.network || "testnet"; }, [addresses, selectedToken]); const requiredCryptoAmount = useMemo(() => { @@ -399,7 +399,7 @@ export default function Purchase({ type }: PurchaseProps) { meterVerified, ]); - // console.log("validation Error", validationError); + console.log("validation Error", validationError); const handleSendWithPin = async (pin: string) => { setErrorMessage(""); diff --git a/components/dashboard/stats-cards.tsx b/components/dashboard/stats-cards.tsx index 72c0ee2..e977b5c 100644 --- a/components/dashboard/stats-cards.tsx +++ b/components/dashboard/stats-cards.tsx @@ -110,10 +110,10 @@ export function StatsCards({ }); return ( -
+
{updatedStats.map((stat, index) => ( {stat.title === "Total Balance" && ( @@ -121,7 +121,7 @@ export function StatsCards({ onClick={handleViewBalance} variant="secondary" size="sm" - className="mt-2 w-fit absolute top-0 right-2" + className="mt-2 w-fit absolute bottom-2 right-2" > {hideBalalance ? : } @@ -129,7 +129,7 @@ export function StatsCards({
-
+
@@ -144,7 +144,7 @@ export function StatsCards({ -------
) : ( -

+

{stat.value}

)} diff --git a/components/dashboard/tabs/dashboard.tsx b/components/dashboard/tabs/dashboard.tsx index 5878cfc..59d23ed 100644 --- a/components/dashboard/tabs/dashboard.tsx +++ b/components/dashboard/tabs/dashboard.tsx @@ -51,19 +51,20 @@ export default function DashboardHome({ activeTab }: DashboardProps) { /> {/* Quick Actions */} - +
+ {" "} +
{/* Main Content Grid */}
+ +
-
- {" "} -
{/* Bottom CTA */} diff --git a/components/dashboard/tabs/send-funds.tsx b/components/dashboard/tabs/send-funds.tsx index 31673d6..c60f55d 100644 --- a/components/dashboard/tabs/send-funds.tsx +++ b/components/dashboard/tabs/send-funds.tsx @@ -121,9 +121,9 @@ export default function SendFunds() { }, [addresses, selectedToken]); const currentNetwork = useMemo(() => { - if (!addresses) return "mainnet"; + if (!addresses) return "testnet"; const addressInfo = addresses.find((addr) => addr.chain === selectedToken); - return addressInfo?.network || "mainnet"; + return addressInfo?.network || "testnet"; }, [addresses, selectedToken]); // Check if selected token has a wallet diff --git a/components/dashboard/wallet-overview.tsx b/components/dashboard/wallet-overview.tsx index 1671cf1..850512e 100644 --- a/components/dashboard/wallet-overview.tsx +++ b/components/dashboard/wallet-overview.tsx @@ -103,7 +103,7 @@ export function WalletOverview({ return ( - + {/* Wallet Overview @@ -115,13 +115,13 @@ export function WalletOverview({ Manage - + */} - + {walletData?.map((wallet, index) => (
@@ -142,12 +142,12 @@ export function WalletOverview({
-

+ {/*

{wallet.chain}

{shortenAddress(wallet.address as `0x${string}`, 6)} -

+

*/} {walletData.length === 0 ? ( ) : ( @@ -174,51 +174,13 @@ export function WalletOverview({

)} -
- - -
)}
))} - {/* Total Balance Summary */} - {breakdown.length > 0 && ( -
-
- Total Value: - - {formatNGN( - breakdown.reduce((sum, item) => sum + item.ngnValue, 0) - )} - -
-
- )} + ); diff --git a/components/hooks/useTokenBalance.ts b/components/hooks/useTokenBalance.ts index cf43067..d4738db 100644 --- a/components/hooks/useTokenBalance.ts +++ b/components/hooks/useTokenBalance.ts @@ -79,10 +79,10 @@ export function useTokenBalance() { const getWalletNetwork = useCallback( (chain: string): string => { - if (!addresses || !Array.isArray(addresses)) return "mainnet"; + if (!addresses || !Array.isArray(addresses)) return "testnet"; const key = normalizeChain(chain); const addressInfo = addresses.find((addr) => normalizeChain(addr.chain) === key); - return addressInfo?.network || "mainnet"; + return addressInfo?.network || "testnet"; }, [addresses, normalizeChain] ); @@ -156,7 +156,7 @@ export function useTokenBalance() { name: getTokenName(chainKey), symbol: entry.symbol || getTokenSymbol(chainKey), address: entry.address || "", - network: entry.network || "mainnet", + network: entry.network || "testnet", balance: typeof entry.balance === "number" ? entry.balance : 0, ngnValue: entry.ngnValue || 0, hasWallet: !!entry.address, @@ -176,7 +176,7 @@ export function useTokenBalance() { name: getTokenName(chain), symbol: match.symbol || getTokenSymbol(chain), address: match.address || "", - network: match.network || "mainnet", + network: match.network || "testnet", balance: parseFloat(match.balance || "0") || 0, ngnValue: 0, hasWallet: !!match.address, diff --git a/lib/api-client.ts b/lib/api-client.ts index e37a97f..ba9d473 100644 --- a/lib/api-client.ts +++ b/lib/api-client.ts @@ -48,7 +48,7 @@ import { GetMerchantPaymentHistoryResponse, } from "@/types/authContext"; -const url = "http://localhost:5500"; +const url = "https://velo-node-backend.onrender.com"; // Service types export interface SupportedNetwork { @@ -343,7 +343,7 @@ class ApiClient { // Wallet methods async getWalletAddresses(): Promise { return this.request<{ addresses: WalletAddress[] }>( - "/wallet/addresses/mainnet", + "/wallet/addresses/testnet", { method: "GET" }, { ttl: 10 * 60 * 1000, @@ -354,7 +354,7 @@ class ApiClient { async getWalletBalances(): Promise { return this.request<{ balances: WalletBalance[] }>( - "/wallet/balances/mainnet", + "/wallet/balances/testnet", { method: "GET" }, { ttl: 2 * 60 * 1000, @@ -370,7 +370,7 @@ class ApiClient { }); // Invalidate balance cache after sending transaction - this.cache.invalidateCache(["/wallet/balances/mainnet"]); + this.cache.invalidateCache(["/wallet/balances/testnet"]); return result; } @@ -630,7 +630,7 @@ class ApiClient { async checkDeploy(): Promise { return this.request( - "/checkdeploy/balances/mainnet/deploy", + "/checkdeploy/balances/testnet/deploy", { method: "GET", }, diff --git a/next.config.ts b/next.config.ts index 112ebaa..3b50c9f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,3 +1,4 @@ +import {withSentryConfig} from '@sentry/nextjs'; /** @type {import('next').NextConfig} */ const nextConfig = { async redirects() { @@ -12,3 +13,36 @@ const nextConfig = { } module.exports = nextConfig + +export default withSentryConfig(undefined, { + // For all available options, see: + // https://www.npmjs.com/package/@sentry/webpack-plugin#options + + org: "velo-7b", + + project: "javascript-nextjs", + + // Only print logs for uploading source maps in CI + silent: !process.env.CI, + + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + + // Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. + // This can increase your server load as well as your hosting bill. + // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- + // side errors will fail. + // tunnelRoute: "/monitoring", + + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, + + // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) + // See the following for more information: + // https://docs.sentry.io/product/crons/ + // https://vercel.com/docs/cron-jobs + automaticVercelMonitors: true, +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8fb4bbe..4db1f8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-visually-hidden": "^1.2.3", + "@sentry/nextjs": "^10.25.0", "@solana/web3.js": "^1.98.4", "@starknet-react/chains": "^4.0.4", "@starknet-react/core": "^4.0.4", @@ -94,6 +95,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@apm-js-collab/code-transformer": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer/-/code-transformer-0.8.2.tgz", + "integrity": "sha512-YRjJjNq5KFSjDUoqu5pFUWrrsvGOxl6c3bu+uMFc9HNNptZ2rNU/TI2nLw4jnhQNtka972Ee2m3uqbvDQtPeCA==", + "license": "Apache-2.0" + }, + "node_modules/@apm-js-collab/tracing-hooks": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.3.1.tgz", + "integrity": "sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw==", + "license": "Apache-2.0", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.8.0", + "debug": "^4.4.1", + "module-details-from-path": "^1.0.4" + } + }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", @@ -817,6 +835,230 @@ "node": ">=18.0.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", @@ -826,6 +1068,51 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@base-org/account": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@base-org/account/-/account-1.1.1.tgz", @@ -2454,7 +2741,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2465,7 +2751,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2476,24 +2761,32 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2862,881 +3155,867 @@ "node": ">=12.4.0" } }, - "node_modules/@phosphor-icons/webcomponents": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@phosphor-icons/webcomponents/-/webcomponents-2.1.5.tgz", - "integrity": "sha512-JcvQkZxvcX2jK+QCclm8+e8HXqtdFW9xV4/kk2aL9Y3dJA2oQVt+pzbv1orkumz3rfx4K9mn9fDoMr1He1yr7Q==", - "license": "MIT", - "dependencies": { - "lit": "^3" + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" } }, - "node_modules/@privy-io/api-base": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@privy-io/api-base/-/api-base-1.7.0.tgz", - "integrity": "sha512-ji6ARQAAuW/FzRTgft9NCjRuouWX9t+J7JMmvLPsnXQJDFEV9mqh4sWXZhQ8ddTq/iDZ4z/yz1ORJqN8bYAi4Q==", + "node_modules/@opentelemetry/api-logs": { + "version": "0.204.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.204.0.tgz", + "integrity": "sha512-DqxY8yoAaiBPivoJD4UtgrMS8gEmzZ5lnaxzPojzLVHBGqPxgWm4zcuvcUHZiqQ6kRX2Klel2r9y8cA2HAtqpw==", + "license": "Apache-2.0", "dependencies": { - "zod": "^3.24.3" + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" } }, - "node_modules/@privy-io/chains": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@privy-io/chains/-/chains-0.0.2.tgz", - "integrity": "sha512-vT+EcPstcKbvrPyGA2YDD1W8YxaJhKFKYGmS9PaycODpL9HvMsPpkJ1y6SddmVAKL+WIow+nH9cV1/q0aCmPXA==", - "license": "Apache-2.0" + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz", + "integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } }, - "node_modules/@privy-io/ethereum": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@privy-io/ethereum/-/ethereum-0.0.2.tgz", - "integrity": "sha512-FnJ1dzgg/tij4jLeKHLlZM9uNk4WN+iIOkc8CG0FZKUQxqXH60Fs/dMF6Xbndd5CQkUO8LUU7FLom/405VKXpQ==", + "node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, "peerDependencies": { - "viem": "^2.21.36" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@privy-io/js-sdk-core": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@privy-io/js-sdk-core/-/js-sdk-core-0.55.0.tgz", - "integrity": "sha512-halUc0CS0fKQY5fX7YOBpL1piBz5eg140rL+tjyL7yeBCjNkgf5DX6J4BWzRdr5AplRdMWRBUb1/6+g+pcm4Iw==", + "node_modules/@opentelemetry/instrumentation": { + "version": "0.204.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.204.0.tgz", + "integrity": "sha512-vV5+WSxktzoMP8JoYWKeopChy6G3HKk4UQ2hESCRDUUTZqQ3+nM3u8noVG0LmNfRWwcFBnbZ71GKC7vaYYdJ1g==", "license": "Apache-2.0", "dependencies": { - "@ethersproject/abstract-signer": "^5.7.0", - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/contracts": "^5.7.0", - "@ethersproject/providers": "^5.7.2", - "@ethersproject/transactions": "^5.7.0", - "@ethersproject/units": "^5.7.0", - "@privy-io/api-base": "1.7.0", - "@privy-io/chains": "0.0.2", - "@privy-io/public-api": "2.45.0", - "canonicalize": "^2.0.0", - "eventemitter3": "^5.0.1", - "fetch-retry": "^6.0.0", - "jose": "^4.15.5", - "js-cookie": "^3.0.5", - "libphonenumber-js": "^1.10.44", - "set-cookie-parser": "^2.6.0", - "uuid": ">=8 <10" + "@opentelemetry/api-logs": "0.204.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "permissionless": "^0.2.47", - "viem": "^2.30.6" + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-amqplib": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.51.0.tgz", + "integrity": "sha512-XGmjYwjVRktD4agFnWBWQXo9SiYHKBxR6Ag3MLXwtLE4R99N3a08kGKM5SC1qOFKIELcQDGFEFT9ydXMH00Luw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, - "peerDependenciesMeta": { - "permissionless": { - "optional": true - }, - "viem": { - "optional": true - } + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@privy-io/public-api": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@privy-io/public-api/-/public-api-2.45.0.tgz", - "integrity": "sha512-gEYclrdjirGqKrJBn1ZXkTuZTL/gK9FjPppoxDJ8/dX1JD4/vXahdt0AdgUfNUf70ZfMdVKYhyos1RkvSLFGYQ==", + "node_modules/@opentelemetry/instrumentation-connect": { + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.48.0.tgz", + "integrity": "sha512-OMjc3SFL4pC16PeK+tDhwP7MRvDPalYCGSvGqUhX5rASkI2H0RuxZHOWElYeXkV0WP+70Gw6JHWac/2Zqwmhdw==", "license": "Apache-2.0", "dependencies": { - "@privy-io/api-base": "1.7.0", - "bs58": "^5.0.0", - "libphonenumber-js": "^1.10.31", - "viem": "^2", - "zod": "^3.24.3" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/connect": "3.4.38" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@privy-io/public-api/node_modules/base-x": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", - "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", - "license": "MIT" - }, - "node_modules/@privy-io/public-api/node_modules/bs58": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", - "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-dataloader": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.22.0.tgz", + "integrity": "sha512-bXnTcwtngQsI1CvodFkTemrrRSQjAjZxqHVc+CJZTDnidT0T6wt3jkKhnsjU/Kkkc0lacr6VdRpCu2CUWa0OKw==", + "license": "Apache-2.0", "dependencies": { - "base-x": "^4.0.0" + "@opentelemetry/instrumentation": "^0.204.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@privy-io/react-auth": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/@privy-io/react-auth/-/react-auth-2.25.0.tgz", - "integrity": "sha512-lcILJaXi2wYgxFsxbvSrIM1Cz8FAumMy90YS4R6wcBNWg79U6S6YTHKMa0U8xJPhJUoKrugAi/LTSifYdTCZjg==", + "node_modules/@opentelemetry/instrumentation-express": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.53.0.tgz", + "integrity": "sha512-r/PBafQmFYRjuxLYEHJ3ze1iBnP2GDA1nXOSS6E02KnYNZAVjj6WcDA1MSthtdAUUK0XnotHvvWM8/qz7DMO5A==", "license": "Apache-2.0", "dependencies": { - "@base-org/account": "^1.1.0", - "@coinbase/wallet-sdk": "4.3.2", - "@floating-ui/react": "^0.26.22", - "@headlessui/react": "^2.2.0", - "@heroicons/react": "^2.1.1", - "@marsidev/react-turnstile": "^0.4.1", - "@metamask/eth-sig-util": "^6.0.0", - "@privy-io/api-base": "1.7.0", - "@privy-io/chains": "0.0.2", - "@privy-io/ethereum": "0.0.2", - "@privy-io/js-sdk-core": "0.55.0", - "@privy-io/public-api": "2.45.0", - "@reown/appkit": "^1.7.11", - "@scure/base": "^1.2.5", - "@simplewebauthn/browser": "^9.0.1", - "@solana/wallet-adapter-base": "0.9.23", - "@solana/wallet-standard-wallet-adapter-base": "^1.1.2", - "@solana/wallet-standard-wallet-adapter-react": "^1.1.2", - "@tanstack/react-virtual": "^3.13.10", - "@wallet-standard/app": "^1.0.1", - "@walletconnect/ethereum-provider": "2.21.7", - "base64-js": "^1.5.1", - "dotenv": "^16.0.3", - "encoding": "^0.1.13", - "eventemitter3": "^5.0.1", - "fast-password-entropy": "^1.1.1", - "jose": "^4.15.5", - "js-cookie": "^3.0.5", - "lokijs": "^1.5.12", - "lucide-react": "^0.383.0", - "md5": "^2.3.0", - "mipd": "^0.0.7", - "ofetch": "^1.3.4", - "pino-pretty": "^10.0.0", - "qrcode": "^1.5.1", - "react-device-detect": "^2.2.2", - "secure-password-utilities": "^0.2.1", - "styled-components": "^6.1.13", - "stylis": "^4.3.4", - "tinycolor2": "^1.6.0", - "uuid": ">=8 <10", - "viem": "^2.32.0", - "zustand": "^5.0.0" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, - "peerDependencies": { - "@abstract-foundation/agw-client": "^1.0.0", - "@solana-program/system": "^0.7.0", - "@solana-program/token": "^0.5.1", - "@solana/kit": "^2.3.0", - "@solana/spl-token": "^0.4.9", - "@solana/web3.js": "^1.95.8", - "permissionless": "^0.2.47", - "react": "^18 || ^19", - "react-dom": "^18 || ^19" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@abstract-foundation/agw-client": { - "optional": true - }, - "@solana-program/system": { - "optional": true - }, - "@solana-program/token": { - "optional": true - }, - "@solana/kit": { - "optional": true - }, - "@solana/spl-token": { - "optional": true - }, - "@solana/web3.js": { - "optional": true - }, - "permissionless": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@privy-io/react-auth/node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", + "node_modules/@opentelemetry/instrumentation-fs": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.24.0.tgz", + "integrity": "sha512-HjIxJ6CBRD770KNVaTdMXIv29Sjz4C1kPCCK5x1Ujpc6SNnLGPqUVyJYZ3LUhhnHAqdbrl83ogVWjCgeT4Q0yw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.204.0" + }, "engines": { - "node": ">=12" + "node": "^18.19.0 || >=20.6.0" }, - "funding": { - "url": "https://dotenvx.com" + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@privy-io/react-auth/node_modules/lucide-react": { - "version": "0.383.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.383.0.tgz", - "integrity": "sha512-13xlG0CQCJtzjSQYwwJ3WRqMHtRj3EXmLlorrARt7y+IHnxUCp3XyFNL1DfaGySWxHObDvnu1u1dV+0VMKHUSg==", - "license": "ISC", + "node_modules/@opentelemetry/instrumentation-generic-pool": { + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.48.0.tgz", + "integrity": "sha512-TLv/On8pufynNR+pUbpkyvuESVASZZKMlqCm4bBImTpXKTpqXaJJ3o/MUDeMlM91rpen+PEv2SeyOKcHCSlgag==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.204.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-graphql": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.52.0.tgz", + "integrity": "sha512-3fEJ8jOOMwopvldY16KuzHbRhPk8wSsOTSF0v2psmOCGewh6ad+ZbkTx/xyUK9rUdUMWAxRVU0tFpj4Wx1vkPA==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@opentelemetry/instrumentation": "^0.204.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", - "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-hapi": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.51.0.tgz", + "integrity": "sha512-qyf27DaFNL1Qhbo/da+04MSCw982B02FhuOS5/UF+PMhM61CcOiu7fPuXj8TvbqyReQuJFljXE6UirlvoT/62g==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.204.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.204.0.tgz", + "integrity": "sha512-1afJYyGRA4OmHTv0FfNTrTAzoEjPQUYgd+8ih/lX0LlZBnGio/O80vxA0lN3knsJPS7FiDrsDrWq25K7oAzbkw==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "@opentelemetry/core": "2.1.0", + "@opentelemetry/instrumentation": "0.204.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.1.0.tgz", + "integrity": "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-ioredis": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.52.0.tgz", + "integrity": "sha512-rUvlyZwI90HRQPYicxpDGhT8setMrlHKokCtBtZgYxQWRF5RBbG4q0pGtbZvd7kyseuHbFpA3I/5z7M8b/5ywg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/redis-common": "^0.38.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.14.0.tgz", + "integrity": "sha512-kbB5yXS47dTIdO/lfbbXlzhvHFturbux4EpP0+6H78Lk0Bn4QXiZQW7rmZY1xBCY16mNcCb8Yt0mhz85hTnSVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/semantic-conventions": "^1.30.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-knex": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.49.0.tgz", + "integrity": "sha512-NKsRRT27fbIYL4Ix+BjjP8h4YveyKc+2gD6DMZbr5R5rUeDqfC8+DTfIt3c3ex3BIc5Vvek4rqHnN7q34ZetLQ==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/semantic-conventions": "^1.33.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node_modules/@opentelemetry/instrumentation-koa": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.52.0.tgz", + "integrity": "sha512-JJSBYLDx/mNSy8Ibi/uQixu2rH0bZODJa8/cz04hEhRaiZQoeJ5UrOhO/mS87IdgVsHrnBOsZ6vDu09znupyuA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-lru-memoizer": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.49.0.tgz", + "integrity": "sha512-ctXu+O/1HSadAxtjoEg2w307Z5iPyLOMM8IRNwjaKrIpNAthYGSOanChbk1kqY6zU5CrpkPHGdAT6jk8dXiMqw==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" + "@opentelemetry/instrumentation": "^0.204.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.57.0.tgz", + "integrity": "sha512-KD6Rg0KSHWDkik+qjIOWoksi1xqSpix8TSPfquIK1DTmd9OTFb5PHmMkzJe16TAPVEuElUW8gvgP59cacFcrMQ==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-mongoose": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.51.0.tgz", + "integrity": "sha512-gwWaAlhhV2By7XcbyU3DOLMvzsgeaymwP/jktDC+/uPkCmgB61zurwqOQdeiRq9KAf22Y2dtE5ZLXxytJRbEVA==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.50.0.tgz", + "integrity": "sha512-duKAvMRI3vq6u9JwzIipY9zHfikN20bX05sL7GjDeLKr2qV0LQ4ADtKST7KStdGcQ+MTN5wghWbbVdLgNcB3rA==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/mysql": "2.15.27" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", - "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-mysql2": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.51.0.tgz", + "integrity": "sha512-zT2Wg22Xn43RyfU3NOUmnFtb5zlDI0fKcijCj9AcK9zuLZ4ModgtLXOyBJSSfO+hsOCZSC1v/Fxwj+nZJFdzLQ==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/sql-common": "^0.41.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.57.0.tgz", + "integrity": "sha512-dWLGE+r5lBgm2A8SaaSYDE3OKJ/kwwy5WLyGyzor8PLhUL9VnJRiY6qhp4njwhnljiLtzeffRtG2Mf/YyWLeTw==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@opentelemetry/sql-common": "^0.41.0", + "@types/pg": "8.15.5", + "@types/pg-pool": "2.0.6" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/react-navigation-menu": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", - "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-redis": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.53.0.tgz", + "integrity": "sha512-WUHV8fr+8yo5RmzyU7D5BIE1zwiaNQcTyZPwtxlfr7px6NYYx7IIpSihJK7WA60npWynfxxK1T67RAVF0Gdfjg==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/redis-common": "^0.38.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-tedious": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.23.0.tgz", + "integrity": "sha512-3TMTk/9VtlRonVTaU4tCzbg4YqW+Iq/l5VnN2e5whP6JgEg/PKfrGbqQ+CxQWNLfLaQYIUgEZqAn5gk/inh1uQ==", + "license": "Apache-2.0", "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/tedious": "^4.0.14" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-undici": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.15.0.tgz", + "integrity": "sha512-sNFGA/iCDlVkNjzTzPRcudmI11vT/WAfAguRdZY9IspCw02N4WSC72zTuQhSMheh2a1gdeM9my1imnKRvEEvEg==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.204.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": "^1.7.0" } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", + "node_modules/@opentelemetry/redis-common": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.2.tgz", + "integrity": "sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", + "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sql-common": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.41.2.tgz", + "integrity": "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@opentelemetry/core": "^2.0.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.19.0 || >=20.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": "^1.1.0" } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "node_modules/@phosphor-icons/webcomponents": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@phosphor-icons/webcomponents/-/webcomponents-2.1.5.tgz", + "integrity": "sha512-JcvQkZxvcX2jK+QCclm8+e8HXqtdFW9xV4/kk2aL9Y3dJA2oQVt+pzbv1orkumz3rfx4K9mn9fDoMr1He1yr7Q==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "lit": "^3" + } + }, + "node_modules/@prisma/instrumentation": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.15.0.tgz", + "integrity": "sha512-6TXaH6OmDkMOQvOxwLZ8XS51hU2v4A3vmE2pSijCIiGRJYyNeMcL6nMHQMyYdZRD8wl7LF3Wzc+AMPMV/9Oo7A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" }, "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "@opentelemetry/api": "^1.8" } }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/api-logs": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz", + "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=14" } }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", + "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/instrumentation": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", + "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@opentelemetry/api-logs": "0.57.2", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">=14" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", + "node_modules/@privy-io/api-base": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@privy-io/api-base/-/api-base-1.7.0.tgz", + "integrity": "sha512-ji6ARQAAuW/FzRTgft9NCjRuouWX9t+J7JMmvLPsnXQJDFEV9mqh4sWXZhQ8ddTq/iDZ4z/yz1ORJqN8bYAi4Q==", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, + "zod": "^3.24.3" + } + }, + "node_modules/@privy-io/chains": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@privy-io/chains/-/chains-0.0.2.tgz", + "integrity": "sha512-vT+EcPstcKbvrPyGA2YDD1W8YxaJhKFKYGmS9PaycODpL9HvMsPpkJ1y6SddmVAKL+WIow+nH9cV1/q0aCmPXA==", + "license": "Apache-2.0" + }, + "node_modules/@privy-io/ethereum": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@privy-io/ethereum/-/ethereum-0.0.2.tgz", + "integrity": "sha512-FnJ1dzgg/tij4jLeKHLlZM9uNk4WN+iIOkc8CG0FZKUQxqXH60Fs/dMF6Xbndd5CQkUO8LUU7FLom/405VKXpQ==", + "license": "Apache-2.0", "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "viem": "^2.21.36" } }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", + "node_modules/@privy-io/js-sdk-core": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@privy-io/js-sdk-core/-/js-sdk-core-0.55.0.tgz", + "integrity": "sha512-halUc0CS0fKQY5fX7YOBpL1piBz5eg140rL+tjyL7yeBCjNkgf5DX6J4BWzRdr5AplRdMWRBUb1/6+g+pcm4Iw==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/contracts": "^5.7.0", + "@ethersproject/providers": "^5.7.2", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/units": "^5.7.0", + "@privy-io/api-base": "1.7.0", + "@privy-io/chains": "0.0.2", + "@privy-io/public-api": "2.45.0", + "canonicalize": "^2.0.0", + "eventemitter3": "^5.0.1", + "fetch-retry": "^6.0.0", + "jose": "^4.15.5", + "js-cookie": "^3.0.5", + "libphonenumber-js": "^1.10.44", + "set-cookie-parser": "^2.6.0", + "uuid": ">=8 <10" }, "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "permissionless": "^0.2.47", + "viem": "^2.30.6" }, "peerDependenciesMeta": { - "@types/react": { + "permissionless": { + "optional": true + }, + "viem": { "optional": true } } }, - "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", - "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", - "license": "MIT", + "node_modules/@privy-io/public-api": { + "version": "2.45.0", + "resolved": "https://registry.npmjs.org/@privy-io/public-api/-/public-api-2.45.0.tgz", + "integrity": "sha512-gEYclrdjirGqKrJBn1ZXkTuZTL/gK9FjPppoxDJ8/dX1JD4/vXahdt0AdgUfNUf70ZfMdVKYhyos1RkvSLFGYQ==", + "license": "Apache-2.0", "dependencies": { - "use-sync-external-store": "^1.5.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "@privy-io/api-base": "1.7.0", + "bs58": "^5.0.0", + "libphonenumber-js": "^1.10.31", + "viem": "^2", + "zod": "^3.24.3" } }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "node_modules/@privy-io/public-api/node_modules/base-x": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", + "license": "MIT" + }, + "node_modules/@privy-io/public-api/node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "license": "MIT", + "dependencies": { + "base-x": "^4.0.0" + } + }, + "node_modules/@privy-io/react-auth": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/@privy-io/react-auth/-/react-auth-2.25.0.tgz", + "integrity": "sha512-lcILJaXi2wYgxFsxbvSrIM1Cz8FAumMy90YS4R6wcBNWg79U6S6YTHKMa0U8xJPhJUoKrugAi/LTSifYdTCZjg==", + "license": "Apache-2.0", + "dependencies": { + "@base-org/account": "^1.1.0", + "@coinbase/wallet-sdk": "4.3.2", + "@floating-ui/react": "^0.26.22", + "@headlessui/react": "^2.2.0", + "@heroicons/react": "^2.1.1", + "@marsidev/react-turnstile": "^0.4.1", + "@metamask/eth-sig-util": "^6.0.0", + "@privy-io/api-base": "1.7.0", + "@privy-io/chains": "0.0.2", + "@privy-io/ethereum": "0.0.2", + "@privy-io/js-sdk-core": "0.55.0", + "@privy-io/public-api": "2.45.0", + "@reown/appkit": "^1.7.11", + "@scure/base": "^1.2.5", + "@simplewebauthn/browser": "^9.0.1", + "@solana/wallet-adapter-base": "0.9.23", + "@solana/wallet-standard-wallet-adapter-base": "^1.1.2", + "@solana/wallet-standard-wallet-adapter-react": "^1.1.2", + "@tanstack/react-virtual": "^3.13.10", + "@wallet-standard/app": "^1.0.1", + "@walletconnect/ethereum-provider": "2.21.7", + "base64-js": "^1.5.1", + "dotenv": "^16.0.3", + "encoding": "^0.1.13", + "eventemitter3": "^5.0.1", + "fast-password-entropy": "^1.1.1", + "jose": "^4.15.5", + "js-cookie": "^3.0.5", + "lokijs": "^1.5.12", + "lucide-react": "^0.383.0", + "md5": "^2.3.0", + "mipd": "^0.0.7", + "ofetch": "^1.3.4", + "pino-pretty": "^10.0.0", + "qrcode": "^1.5.1", + "react-device-detect": "^2.2.2", + "secure-password-utilities": "^0.2.1", + "styled-components": "^6.1.13", + "stylis": "^4.3.4", + "tinycolor2": "^1.6.0", + "uuid": ">=8 <10", + "viem": "^2.32.0", + "zustand": "^5.0.0" + }, + "peerDependencies": { + "@abstract-foundation/agw-client": "^1.0.0", + "@solana-program/system": "^0.7.0", + "@solana-program/token": "^0.5.1", + "@solana/kit": "^2.3.0", + "@solana/spl-token": "^0.4.9", + "@solana/web3.js": "^1.95.8", + "permissionless": "^0.2.47", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@abstract-foundation/agw-client": { + "optional": true + }, + "@solana-program/system": { + "optional": true + }, + "@solana-program/token": { + "optional": true + }, + "@solana/kit": { + "optional": true + }, + "@solana/spl-token": { + "optional": true + }, + "@solana/web3.js": { + "optional": true + }, + "permissionless": { + "optional": true + } + } + }, + "node_modules/@privy-io/react-auth/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@privy-io/react-auth/node_modules/lucide-react": { + "version": "0.383.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.383.0.tgz", + "integrity": "sha512-13xlG0CQCJtzjSQYwwJ3WRqMHtRj3EXmLlorrARt7y+IHnxUCp3XyFNL1DfaGySWxHObDvnu1u1dV+0VMKHUSg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", "license": "MIT", "dependencies": { - "@radix-ui/rect": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3747,13 +4026,26 @@ } } }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", @@ -3770,804 +4062,2234 @@ } } }, - "node_modules/@radix-ui/rect": { + "node_modules/@radix-ui/react-direction": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@react-aria/focus": { - "version": "3.21.2", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz", - "integrity": "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/interactions": "^3.25.6", - "@react-aria/utils": "^3.31.0", - "@react-types/shared": "^3.32.1", - "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" - }, + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/interactions": { - "version": "3.25.6", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.6.tgz", - "integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/ssr": "^3.9.10", - "@react-aria/utils": "^3.31.0", - "@react-stately/flags": "^3.1.2", - "@react-types/shared": "^3.32.1", - "@swc/helpers": "^0.5.0" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@react-aria/ssr": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", - "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "engines": { - "node": ">= 12" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-aria/utils": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.31.0.tgz", - "integrity": "sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", "dependencies": { - "@react-aria/ssr": "^3.9.10", - "@react-stately/flags": "^3.1.2", - "@react-stately/utils": "^3.10.8", - "@react-types/shared": "^3.32.1", - "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-stately/flags": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", - "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@react-stately/utils": { - "version": "3.10.8", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz", - "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.0" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-types/shared": { - "version": "3.32.1", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", - "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@reown/appkit": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/@reown/appkit/-/appkit-1.8.11.tgz", - "integrity": "sha512-G96zt1P3ASrivV7HRrFQuzkCEzSmy3ca7cSQxaLeIS+P2VVhjE5YAyINArGVbooY0heHCPvQuTNM+YgK4oBmfg==", - "hasInstallScript": true, - "license": "SEE LICENSE IN LICENSE.md", + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", "dependencies": { - "@reown/appkit-common": "1.8.11", - "@reown/appkit-controllers": "1.8.11", - "@reown/appkit-pay": "1.8.11", - "@reown/appkit-polyfills": "1.8.11", - "@reown/appkit-scaffold-ui": "1.8.11", - "@reown/appkit-ui": "1.8.11", - "@reown/appkit-utils": "1.8.11", - "@reown/appkit-wallet": "1.8.11", - "@walletconnect/universal-provider": "2.22.4", - "bs58": "6.0.0", - "semver": "7.7.2", - "valtio": "2.1.7", - "viem": ">=2.37.9" + "@radix-ui/react-primitive": "2.1.3" }, - "optionalDependencies": { - "@lit/react": "1.0.8" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@reown/appkit-common": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/@reown/appkit-common/-/appkit-common-1.8.11.tgz", - "integrity": "sha512-rRcxrah6uouqEo/VbniVH11Y3H27BsP+Psv2+Usic+3Rt4kiSImIyeDG1YBV0gZNmME9N3sXHK8Bt7iqkxdOWw==", - "license": "SEE LICENSE IN LICENSE.md", + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", "dependencies": { - "big.js": "6.2.2", - "dayjs": "1.11.13", - "viem": ">=2.37.9" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@reown/appkit-controllers": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/@reown/appkit-controllers/-/appkit-controllers-1.8.11.tgz", - "integrity": "sha512-V3hPB6NE7kM+7pS8n4ygZWbMh/XoHYxdPWxH3qtDlvYlH9Rgc3yvU8+IFWdB79Ta0lyJIbGqVlQeH5sQe4QOsw==", - "license": "SEE LICENSE IN LICENSE.md", + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", "dependencies": { - "@reown/appkit-common": "1.8.11", - "@reown/appkit-wallet": "1.8.11", - "@walletconnect/universal-provider": "2.22.4", - "valtio": "2.1.7", - "viem": ">=2.37.9" - } - }, - "node_modules/@reown/appkit-pay": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/@reown/appkit-pay/-/appkit-pay-1.8.11.tgz", - "integrity": "sha512-68IB3sKfxlCwLz44jvWWpULnmyIGHwnItojrr/PRXUof3z9t/nV8G7FiRY4ZDIo75EkabcIguhtYNSQ7R3vZbA==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@reown/appkit-common": "1.8.11", - "@reown/appkit-controllers": "1.8.11", - "@reown/appkit-ui": "1.8.11", - "@reown/appkit-utils": "1.8.11", - "lit": "3.3.0", - "valtio": "2.1.7" - } - }, - "node_modules/@reown/appkit-polyfills": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/@reown/appkit-polyfills/-/appkit-polyfills-1.8.11.tgz", - "integrity": "sha512-pP9k5dvtWil88Zv3UgGurtbUmTx47z/5eriClGf8JI0VjBu/IExbAHg2gIZNEFdvkFD5/fIqIg8zno46SKbCKQ==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "buffer": "6.0.3" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@reown/appkit-scaffold-ui": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/@reown/appkit-scaffold-ui/-/appkit-scaffold-ui-1.8.11.tgz", - "integrity": "sha512-oCEhNdUh2d59UHp/rLwfzv5odWg0pw000fg4Z53orjnKzcZfBpBDM00I9hu2pijnaYitJVqwsqCQ1F1q6hju/Q==", - "license": "SEE LICENSE IN LICENSE.md", + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", "dependencies": { - "@reown/appkit-common": "1.8.11", - "@reown/appkit-controllers": "1.8.11", - "@reown/appkit-ui": "1.8.11", - "@reown/appkit-utils": "1.8.11", - "@reown/appkit-wallet": "1.8.11", - "lit": "3.3.0" + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@reown/appkit-ui": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/@reown/appkit-ui/-/appkit-ui-1.8.11.tgz", - "integrity": "sha512-kBWZCiGaB/M2exIiDglsaTWYsWR0L89WXi7IFA6RgW0B0piYv5eiS3l/KPNj9/zxK8UnKsGsMefxl/UK/QqMng==", - "license": "SEE LICENSE IN LICENSE.md", + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", "dependencies": { - "@phosphor-icons/webcomponents": "2.1.5", - "@reown/appkit-common": "1.8.11", - "@reown/appkit-controllers": "1.8.11", - "@reown/appkit-wallet": "1.8.11", - "lit": "3.3.0", - "qrcode": "1.5.3" + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@reown/appkit-ui/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@reown/appkit-ui/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "@radix-ui/react-slot": "1.2.3" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@reown/appkit-ui/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@reown/appkit-ui/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "@radix-ui/react-compose-refs": "1.1.2" }, - "engines": { - "node": ">=6" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@reown/appkit-ui/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "engines": { - "node": ">=8" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@reown/appkit-ui/node_modules/qrcode": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", - "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@react-aria/focus": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz", + "integrity": "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.25.6", + "@react-aria/utils": "^3.31.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.25.6", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.6.tgz", + "integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.31.0", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.31.0.tgz", + "integrity": "sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.10.8", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.8", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz", + "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@reown/appkit": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@reown/appkit/-/appkit-1.8.11.tgz", + "integrity": "sha512-G96zt1P3ASrivV7HRrFQuzkCEzSmy3ca7cSQxaLeIS+P2VVhjE5YAyINArGVbooY0heHCPvQuTNM+YgK4oBmfg==", + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.11", + "@reown/appkit-controllers": "1.8.11", + "@reown/appkit-pay": "1.8.11", + "@reown/appkit-polyfills": "1.8.11", + "@reown/appkit-scaffold-ui": "1.8.11", + "@reown/appkit-ui": "1.8.11", + "@reown/appkit-utils": "1.8.11", + "@reown/appkit-wallet": "1.8.11", + "@walletconnect/universal-provider": "2.22.4", + "bs58": "6.0.0", + "semver": "7.7.2", + "valtio": "2.1.7", + "viem": ">=2.37.9" + }, + "optionalDependencies": { + "@lit/react": "1.0.8" + } + }, + "node_modules/@reown/appkit-common": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@reown/appkit-common/-/appkit-common-1.8.11.tgz", + "integrity": "sha512-rRcxrah6uouqEo/VbniVH11Y3H27BsP+Psv2+Usic+3Rt4kiSImIyeDG1YBV0gZNmME9N3sXHK8Bt7iqkxdOWw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "big.js": "6.2.2", + "dayjs": "1.11.13", + "viem": ">=2.37.9" + } + }, + "node_modules/@reown/appkit-controllers": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@reown/appkit-controllers/-/appkit-controllers-1.8.11.tgz", + "integrity": "sha512-V3hPB6NE7kM+7pS8n4ygZWbMh/XoHYxdPWxH3qtDlvYlH9Rgc3yvU8+IFWdB79Ta0lyJIbGqVlQeH5sQe4QOsw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.11", + "@reown/appkit-wallet": "1.8.11", + "@walletconnect/universal-provider": "2.22.4", + "valtio": "2.1.7", + "viem": ">=2.37.9" + } + }, + "node_modules/@reown/appkit-pay": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@reown/appkit-pay/-/appkit-pay-1.8.11.tgz", + "integrity": "sha512-68IB3sKfxlCwLz44jvWWpULnmyIGHwnItojrr/PRXUof3z9t/nV8G7FiRY4ZDIo75EkabcIguhtYNSQ7R3vZbA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.11", + "@reown/appkit-controllers": "1.8.11", + "@reown/appkit-ui": "1.8.11", + "@reown/appkit-utils": "1.8.11", + "lit": "3.3.0", + "valtio": "2.1.7" + } + }, + "node_modules/@reown/appkit-polyfills": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@reown/appkit-polyfills/-/appkit-polyfills-1.8.11.tgz", + "integrity": "sha512-pP9k5dvtWil88Zv3UgGurtbUmTx47z/5eriClGf8JI0VjBu/IExbAHg2gIZNEFdvkFD5/fIqIg8zno46SKbCKQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "buffer": "6.0.3" + } + }, + "node_modules/@reown/appkit-scaffold-ui": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@reown/appkit-scaffold-ui/-/appkit-scaffold-ui-1.8.11.tgz", + "integrity": "sha512-oCEhNdUh2d59UHp/rLwfzv5odWg0pw000fg4Z53orjnKzcZfBpBDM00I9hu2pijnaYitJVqwsqCQ1F1q6hju/Q==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.11", + "@reown/appkit-controllers": "1.8.11", + "@reown/appkit-ui": "1.8.11", + "@reown/appkit-utils": "1.8.11", + "@reown/appkit-wallet": "1.8.11", + "lit": "3.3.0" + } + }, + "node_modules/@reown/appkit-ui": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@reown/appkit-ui/-/appkit-ui-1.8.11.tgz", + "integrity": "sha512-kBWZCiGaB/M2exIiDglsaTWYsWR0L89WXi7IFA6RgW0B0piYv5eiS3l/KPNj9/zxK8UnKsGsMefxl/UK/QqMng==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@phosphor-icons/webcomponents": "2.1.5", + "@reown/appkit-common": "1.8.11", + "@reown/appkit-controllers": "1.8.11", + "@reown/appkit-wallet": "1.8.11", + "lit": "3.3.0", + "qrcode": "1.5.3" + } + }, + "node_modules/@reown/appkit-ui/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/@reown/appkit-ui/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@reown/appkit-ui/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@reown/appkit-ui/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@reown/appkit-ui/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@reown/appkit-ui/node_modules/qrcode": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@reown/appkit-ui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@reown/appkit-ui/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/@reown/appkit-ui/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@reown/appkit-ui/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@reown/appkit-utils": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@reown/appkit-utils/-/appkit-utils-1.8.11.tgz", + "integrity": "sha512-6Wplz7LNe2HoxHmGGYT6FIp8saa0DzVP/427jlNuWpkL65/azPyYjA8tKptxbFWViQ4VYBsqLoQxivXC/xNMBg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.11", + "@reown/appkit-controllers": "1.8.11", + "@reown/appkit-polyfills": "1.8.11", + "@reown/appkit-wallet": "1.8.11", + "@wallet-standard/wallet": "1.1.0", + "@walletconnect/logger": "^3.0.0", + "@walletconnect/universal-provider": "2.22.4", + "valtio": "2.1.7", + "viem": ">=2.37.9" + }, + "peerDependencies": { + "valtio": "2.1.7" + } + }, + "node_modules/@reown/appkit-utils/node_modules/@walletconnect/logger": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-3.0.0.tgz", + "integrity": "sha512-DDktPBFdmt5d7U3sbp4e3fQHNS1b6amsR8FmtOnt6L2SnV7VfcZr8VmAGL12zetAR+4fndegbREmX0P8Mw6eDg==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.2", + "pino": "10.0.0" + } + }, + "node_modules/@reown/appkit-utils/node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@reown/appkit-utils/node_modules/pino": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.0.0.tgz", + "integrity": "sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "slow-redact": "^0.3.0", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/@reown/appkit-utils/node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/@reown/appkit-utils/node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/@reown/appkit-utils/node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@reown/appkit-utils/node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@reown/appkit-utils/node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/@reown/appkit-utils/node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/@reown/appkit-wallet": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@reown/appkit-wallet/-/appkit-wallet-1.8.11.tgz", + "integrity": "sha512-tCzrieMuOD4tDcNQjMe83T38tJUfRdv4LG+cYGyGK1RpPGaszbq06TQVbCPkRrGiTELwkrKwXjSg1XAuHktS2w==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.11", + "@reown/appkit-polyfills": "1.8.11", + "@walletconnect/logger": "^3.0.0", + "zod": "3.22.4" + } + }, + "node_modules/@reown/appkit-wallet/node_modules/@walletconnect/logger": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-3.0.0.tgz", + "integrity": "sha512-DDktPBFdmt5d7U3sbp4e3fQHNS1b6amsR8FmtOnt6L2SnV7VfcZr8VmAGL12zetAR+4fndegbREmX0P8Mw6eDg==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.2", + "pino": "10.0.0" + } + }, + "node_modules/@reown/appkit-wallet/node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@reown/appkit-wallet/node_modules/pino": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.0.0.tgz", + "integrity": "sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "slow-redact": "^0.3.0", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/@reown/appkit-wallet/node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/@reown/appkit-wallet/node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/@reown/appkit-wallet/node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@reown/appkit-wallet/node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@reown/appkit-wallet/node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/@reown/appkit-wallet/node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/@reown/appkit-wallet/node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@reown/appkit/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", + "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rometools/cli-darwin-arm64": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/@rometools/cli-darwin-arm64/-/cli-darwin-arm64-12.1.3.tgz", + "integrity": "sha512-AmFTUDYjBuEGQp/Wwps+2cqUr+qhR7gyXAUnkL5psCuNCz3807TrUq/ecOoct5MIavGJTH6R4aaSL6+f+VlBEg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rometools/cli-darwin-x64": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/@rometools/cli-darwin-x64/-/cli-darwin-x64-12.1.3.tgz", + "integrity": "sha512-k8MbWna8q4LRlb005N2X+JS1UQ+s3ZLBBvwk4fP8TBxlAJXUz17jLLu/Fi+7DTTEmMhM84TWj4FDKW+rNar28g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rometools/cli-linux-arm64": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/@rometools/cli-linux-arm64/-/cli-linux-arm64-12.1.3.tgz", + "integrity": "sha512-X/uLhJ2/FNA3nu5TiyeNPqiD3OZoFfNfRvw6a3ut0jEREPvEn72NI7WPijH/gxSz55znfQ7UQ6iM4DZumUknJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rometools/cli-linux-x64": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/@rometools/cli-linux-x64/-/cli-linux-x64-12.1.3.tgz", + "integrity": "sha512-csP17q1eWiUXx9z6Jr/JJPibkplyKIwiWPYNzvPCGE8pHlKhwZj3YHRuu7Dm/4EOqx0XFIuqqWZUYm9bkIC8xg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rometools/cli-win32-arm64": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/@rometools/cli-win32-arm64/-/cli-win32-arm64-12.1.3.tgz", + "integrity": "sha512-RymHWeod57EBOJY4P636CgUwYA6BQdkQjh56XKk4pLEHO6X1bFyMet2XL7KlHw5qOTalzuzf5jJqUs+vf3jdXQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rometools/cli-win32-x64": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/@rometools/cli-win32-x64/-/cli-win32-x64-12.1.3.tgz", + "integrity": "sha512-yHSKYidqJMV9nADqg78GYA+cZ0hS1twANAjiFibQdXj9aGzD+s/IzIFEIi/U/OBLvWYg/SCw0QVozi2vTlKFDQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.13.0.tgz", + "integrity": "sha512-2ih5qGw5SZJ+2fLZxP6Lr6Na2NTIgPRL/7Kmyuw0uIyBQnuhQ8fi8fzUTd38eIQmqp+GYLC00cI6WgtqHxBwmw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "encode-utf8": "^1.0.3", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@reown/appkit-ui/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@reown/appkit-ui/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" - }, - "node_modules/@reown/appkit-ui/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", "license": "MIT", "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@reown/appkit-ui/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "@noble/hashes": "1.4.0" }, - "engines": { - "node": ">=6" + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@reown/appkit-utils": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/@reown/appkit-utils/-/appkit-utils-1.8.11.tgz", - "integrity": "sha512-6Wplz7LNe2HoxHmGGYT6FIp8saa0DzVP/427jlNuWpkL65/azPyYjA8tKptxbFWViQ4VYBsqLoQxivXC/xNMBg==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@reown/appkit-common": "1.8.11", - "@reown/appkit-controllers": "1.8.11", - "@reown/appkit-polyfills": "1.8.11", - "@reown/appkit-wallet": "1.8.11", - "@wallet-standard/wallet": "1.1.0", - "@walletconnect/logger": "^3.0.0", - "@walletconnect/universal-provider": "2.22.4", - "valtio": "2.1.7", - "viem": ">=2.37.9" - }, - "peerDependencies": { - "valtio": "2.1.7" + "node_modules/@scure/bip32/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/logger": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-3.0.0.tgz", - "integrity": "sha512-DDktPBFdmt5d7U3sbp4e3fQHNS1b6amsR8FmtOnt6L2SnV7VfcZr8VmAGL12zetAR+4fndegbREmX0P8Mw6eDg==", + "node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", "license": "MIT", "dependencies": { - "@walletconnect/safe-json": "^1.0.2", - "pino": "10.0.0" + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@reown/appkit-utils/node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "node_modules/@scure/bip39/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", "license": "MIT", - "engines": { - "node": ">=14.0.0" + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@reown/appkit-utils/node_modules/pino": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-10.0.0.tgz", - "integrity": "sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA==", + "node_modules/@scure/starknet": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@scure/starknet/-/starknet-1.1.0.tgz", + "integrity": "sha512-83g3M6Ix2qRsPN4wqLDqiRZ2GBNbjVWfboJE/9UjfG+MHr6oDSu/CWgy8hsBSJejr09DkkL+l0Ze4KVrlCIdtQ==", "license": "MIT", "dependencies": { - "atomic-sleep": "^1.0.0", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "slow-redact": "^0.3.0", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" + "@noble/curves": "~1.7.0", + "@noble/hashes": "~1.6.0" }, - "bin": { - "pino": "bin.js" + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@reown/appkit-utils/node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "node_modules/@scure/starknet/node_modules/@noble/curves": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz", + "integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==", "license": "MIT", "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/@reown/appkit-utils/node_modules/pino-std-serializers": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", - "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", - "license": "MIT" - }, - "node_modules/@reown/appkit-utils/node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/@reown/appkit-utils/node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "license": "MIT", + "@noble/hashes": "1.6.0" + }, "engines": { - "node": ">= 12.13.0" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@reown/appkit-utils/node_modules/sonic-boom": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", - "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "node_modules/@scure/starknet/node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", + "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==", "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@reown/appkit-utils/node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "node_modules/@scure/starknet/node_modules/@noble/hashes": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz", + "integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==", "license": "MIT", - "dependencies": { - "real-require": "^0.2.0" - } - }, - "node_modules/@reown/appkit-wallet": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/@reown/appkit-wallet/-/appkit-wallet-1.8.11.tgz", - "integrity": "sha512-tCzrieMuOD4tDcNQjMe83T38tJUfRdv4LG+cYGyGK1RpPGaszbq06TQVbCPkRrGiTELwkrKwXjSg1XAuHktS2w==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@reown/appkit-common": "1.8.11", - "@reown/appkit-polyfills": "1.8.11", - "@walletconnect/logger": "^3.0.0", - "zod": "3.22.4" + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@reown/appkit-wallet/node_modules/@walletconnect/logger": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-3.0.0.tgz", - "integrity": "sha512-DDktPBFdmt5d7U3sbp4e3fQHNS1b6amsR8FmtOnt6L2SnV7VfcZr8VmAGL12zetAR+4fndegbREmX0P8Mw6eDg==", + "node_modules/@sentry-internal/browser-utils": { + "version": "10.25.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.25.0.tgz", + "integrity": "sha512-wzg1ITZxrRtQouHPCgpt3tl1GiNAWFVy2RYK2KstFEhpYBAOUn9BAdP7KU9UyHBFKqbAvV4oGtAT8H2/Y4+leA==", "license": "MIT", - "dependencies": { - "@walletconnect/safe-json": "^1.0.2", - "pino": "10.0.0" + "dependencies": { + "@sentry/core": "10.25.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@reown/appkit-wallet/node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "node_modules/@sentry-internal/feedback": { + "version": "10.25.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.25.0.tgz", + "integrity": "sha512-qlbT4tOd+WRyKpLdsbi26rkynGBoVabnY8/9rFnTxZ0WIUG5EFhJFqEeRLMyv+uk0uRFF3H0I9+u+qP/BKxIcQ==", "license": "MIT", + "dependencies": { + "@sentry/core": "10.25.0" + }, "engines": { - "node": ">=14.0.0" + "node": ">=18" } }, - "node_modules/@reown/appkit-wallet/node_modules/pino": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-10.0.0.tgz", - "integrity": "sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA==", + "node_modules/@sentry-internal/replay": { + "version": "10.25.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.25.0.tgz", + "integrity": "sha512-V/kKQn9T46HBTiP0bIThmpVr94K4vXwYM3/EHVpGSq4P9RynX06cgps8GLHq94+A0kX/DbK9igEMZmIuzS1q3A==", "license": "MIT", "dependencies": { - "atomic-sleep": "^1.0.0", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "slow-redact": "^0.3.0", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" + "@sentry-internal/browser-utils": "10.25.0", + "@sentry/core": "10.25.0" }, - "bin": { - "pino": "bin.js" + "engines": { + "node": ">=18" } }, - "node_modules/@reown/appkit-wallet/node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "node_modules/@sentry-internal/replay-canvas": { + "version": "10.25.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.25.0.tgz", + "integrity": "sha512-zuj5jVNswZ/aA1nPPbU+VIFkQG0695lbyIfS1Skq+5o2FdRIS3MGnBXw1abI9h4pft8GLQWcKiBxISM7UpSz6w==", "license": "MIT", "dependencies": { - "split2": "^4.0.0" + "@sentry-internal/replay": "10.25.0", + "@sentry/core": "10.25.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@reown/appkit-wallet/node_modules/pino-std-serializers": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", - "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", - "license": "MIT" - }, - "node_modules/@reown/appkit-wallet/node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/@reown/appkit-wallet/node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "node_modules/@sentry/babel-plugin-component-annotate": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.6.0.tgz", + "integrity": "sha512-3soTX50JPQQ51FSbb4qvNBf4z/yP7jTdn43vMTp9E4IxvJ9HKJR7OEuKkCMszrZmWsVABXl02msqO7QisePdiQ==", "license": "MIT", "engines": { - "node": ">= 12.13.0" + "node": ">= 14" } }, - "node_modules/@reown/appkit-wallet/node_modules/sonic-boom": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", - "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "node_modules/@sentry/browser": { + "version": "10.25.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.25.0.tgz", + "integrity": "sha512-UgSVT3RTM3vsK914TPuHVJQsjq5ooXVmjMtsWP3Ep+6f7N+1UVX4ZXsyyj5lDOcWdc79FgproD+MrEf9Cj6uBg==", "license": "MIT", "dependencies": { - "atomic-sleep": "^1.0.0" + "@sentry-internal/browser-utils": "10.25.0", + "@sentry-internal/feedback": "10.25.0", + "@sentry-internal/replay": "10.25.0", + "@sentry-internal/replay-canvas": "10.25.0", + "@sentry/core": "10.25.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@reown/appkit-wallet/node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "node_modules/@sentry/bundler-plugin-core": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.6.0.tgz", + "integrity": "sha512-Fub2XQqrS258jjS8qAxLLU1k1h5UCNJ76i8m4qZJJdogWWaF8t00KnnTyp9TEDJzrVD64tRXS8+HHENxmeUo3g==", "license": "MIT", "dependencies": { - "real-require": "^0.2.0" + "@babel/core": "^7.18.5", + "@sentry/babel-plugin-component-annotate": "4.6.0", + "@sentry/cli": "^2.57.0", + "dotenv": "^16.3.1", + "find-up": "^5.0.0", + "glob": "^9.3.2", + "magic-string": "0.30.8", + "unplugin": "1.0.1" + }, + "engines": { + "node": ">= 14" } }, - "node_modules/@reown/appkit-wallet/node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "license": "MIT", + "node_modules/@sentry/bundler-plugin-core/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, "funding": { - "url": "https://github.com/sponsors/colinhacks" + "url": "https://dotenvx.com" } }, - "node_modules/@reown/appkit/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", + "node_modules/@sentry/bundler-plugin-core/node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sentry/cli": { + "version": "2.58.2", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.2.tgz", + "integrity": "sha512-U4u62V4vaTWF+o40Mih8aOpQKqKUbZQt9A3LorIJwaE3tO3XFLRI70eWtW2se1Qmy0RZ74zB14nYcFNFl2t4Rw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.7", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "which": "^2.0.2" + }, "bin": { - "semver": "bin/semver.js" + "sentry-cli": "bin/sentry-cli" + }, + "engines": { + "node": ">= 10" }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.58.2", + "@sentry/cli-linux-arm": "2.58.2", + "@sentry/cli-linux-arm64": "2.58.2", + "@sentry/cli-linux-i686": "2.58.2", + "@sentry/cli-linux-x64": "2.58.2", + "@sentry/cli-win32-arm64": "2.58.2", + "@sentry/cli-win32-i686": "2.58.2", + "@sentry/cli-win32-x64": "2.58.2" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.58.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.2.tgz", + "integrity": "sha512-MArsb3zLhA2/cbd4rTm09SmTpnEuZCoZOpuZYkrpDw1qzBVJmRFA1W1hGAQ9puzBIk/ubY3EUhhzuU3zN2uD6w==", + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "darwin" + ], "engines": { "node": ">=10" } }, - "node_modules/@rometools/cli-darwin-arm64": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/@rometools/cli-darwin-arm64/-/cli-darwin-arm64-12.1.3.tgz", - "integrity": "sha512-AmFTUDYjBuEGQp/Wwps+2cqUr+qhR7gyXAUnkL5psCuNCz3807TrUq/ecOoct5MIavGJTH6R4aaSL6+f+VlBEg==", + "node_modules/@sentry/cli-linux-arm": { + "version": "2.58.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.2.tgz", + "integrity": "sha512-HU9lTCzcHqCz/7Mt5n+cv+nFuJdc1hGD2h35Uo92GgxX3/IujNvOUfF+nMX9j6BXH6hUt73R5c0Ycq9+a3Parg==", "cpu": [ - "arm64" + "arm" ], - "license": "MIT", + "license": "BSD-3-Clause", "optional": true, "os": [ - "darwin" - ] + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } }, - "node_modules/@rometools/cli-darwin-x64": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/@rometools/cli-darwin-x64/-/cli-darwin-x64-12.1.3.tgz", - "integrity": "sha512-k8MbWna8q4LRlb005N2X+JS1UQ+s3ZLBBvwk4fP8TBxlAJXUz17jLLu/Fi+7DTTEmMhM84TWj4FDKW+rNar28g==", + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.58.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.2.tgz", + "integrity": "sha512-ay3OeObnbbPrt45cjeUyQjsx5ain1laj1tRszWj37NkKu55NZSp4QCg1gGBZ0gBGhckI9nInEsmKtix00alw2g==", "cpu": [ - "x64" + "arm64" ], - "license": "MIT", + "license": "BSD-3-Clause", "optional": true, "os": [ - "darwin" - ] + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } }, - "node_modules/@rometools/cli-linux-arm64": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/@rometools/cli-linux-arm64/-/cli-linux-arm64-12.1.3.tgz", - "integrity": "sha512-X/uLhJ2/FNA3nu5TiyeNPqiD3OZoFfNfRvw6a3ut0jEREPvEn72NI7WPijH/gxSz55znfQ7UQ6iM4DZumUknJg==", + "node_modules/@sentry/cli-linux-i686": { + "version": "2.58.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.2.tgz", + "integrity": "sha512-CN9p0nfDFsAT1tTGBbzOUGkIllwS3hygOUyTK7LIm9z+UHw5uNgNVqdM/3Vg+02ymjkjISNB3/+mqEM5osGXdA==", "cpu": [ - "arm64" + "x86", + "ia32" ], - "license": "MIT", + "license": "BSD-3-Clause", "optional": true, "os": [ - "linux" - ] + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } }, - "node_modules/@rometools/cli-linux-x64": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/@rometools/cli-linux-x64/-/cli-linux-x64-12.1.3.tgz", - "integrity": "sha512-csP17q1eWiUXx9z6Jr/JJPibkplyKIwiWPYNzvPCGE8pHlKhwZj3YHRuu7Dm/4EOqx0XFIuqqWZUYm9bkIC8xg==", + "node_modules/@sentry/cli-linux-x64": { + "version": "2.58.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.2.tgz", + "integrity": "sha512-oX/LLfvWaJO50oBVOn4ZvG2SDWPq0MN8SV9eg5tt2nviq+Ryltfr7Rtoo+HfV+eyOlx1/ZXhq9Wm7OT3cQuz+A==", "cpu": [ "x64" ], - "license": "MIT", + "license": "BSD-3-Clause", "optional": true, "os": [ - "linux" - ] + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } }, - "node_modules/@rometools/cli-win32-arm64": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/@rometools/cli-win32-arm64/-/cli-win32-arm64-12.1.3.tgz", - "integrity": "sha512-RymHWeod57EBOJY4P636CgUwYA6BQdkQjh56XKk4pLEHO6X1bFyMet2XL7KlHw5qOTalzuzf5jJqUs+vf3jdXQ==", + "node_modules/@sentry/cli-win32-arm64": { + "version": "2.58.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.2.tgz", + "integrity": "sha512-+cl3x2HPVMpoSVGVM1IDWlAEREZrrVQj4xBb0TRKII7g3hUxRsAIcsrr7+tSkie++0FuH4go/b5fGAv51OEF3w==", "cpu": [ "arm64" ], - "license": "MIT", + "license": "BSD-3-Clause", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">=10" + } }, - "node_modules/@rometools/cli-win32-x64": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/@rometools/cli-win32-x64/-/cli-win32-x64-12.1.3.tgz", - "integrity": "sha512-yHSKYidqJMV9nADqg78GYA+cZ0hS1twANAjiFibQdXj9aGzD+s/IzIFEIi/U/OBLvWYg/SCw0QVozi2vTlKFDQ==", + "node_modules/@sentry/cli-win32-i686": { + "version": "2.58.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.2.tgz", + "integrity": "sha512-omFVr0FhzJ8oTJSg1Kf+gjLgzpYklY0XPfLxZ5iiMiYUKwF5uo1RJRdkUOiEAv0IqpUKnmKcmVCLaDxsWclB7Q==", "cpu": [ - "x64" + "x86", + "ia32" ], - "license": "MIT", + "license": "BSD-3-Clause", "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" + ], + "engines": { + "node": ">=10" + } }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.13.0.tgz", - "integrity": "sha512-2ih5qGw5SZJ+2fLZxP6Lr6Na2NTIgPRL/7Kmyuw0uIyBQnuhQ8fi8fzUTd38eIQmqp+GYLC00cI6WgtqHxBwmw==", - "dev": true, - "license": "MIT" + "node_modules/@sentry/cli-win32-x64": { + "version": "2.58.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.2.tgz", + "integrity": "sha512-2NAFs9UxVbRztQbgJSP5i8TB9eJQ7xraciwj/93djrSMHSEbJ0vC47TME0iifgvhlHMs5vqETOKJtfbbpQAQFA==", + "cpu": [ + "x64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } }, - "node_modules/@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "node_modules/@sentry/core": { + "version": "10.25.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.25.0.tgz", + "integrity": "sha512-mGi4BYIPwZjWdOXHrPoXz1AW4/cQbFoiuW/m+OOATmtSoGTDnWYwP+qZU7VLlL+v8ZEzxfPi2C1NPfJtPj7QWA==", "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=18" } }, - "node_modules/@scure/bip32": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", - "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "node_modules/@sentry/nextjs": { + "version": "10.25.0", + "resolved": "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-10.25.0.tgz", + "integrity": "sha512-OQGjSNOS3UJqFyRlLA1mTEvHWo4ZYxCNDVsX9X4iPBYwjkFAfpmusvifvnBqAJ8um9bndTs2VnVDCWArjSY6vA==", "license": "MIT", "dependencies": { - "@noble/curves": "~1.4.0", - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/semantic-conventions": "^1.37.0", + "@rollup/plugin-commonjs": "28.0.1", + "@sentry-internal/browser-utils": "10.25.0", + "@sentry/bundler-plugin-core": "^4.3.0", + "@sentry/core": "10.25.0", + "@sentry/node": "10.25.0", + "@sentry/opentelemetry": "10.25.0", + "@sentry/react": "10.25.0", + "@sentry/vercel-edge": "10.25.0", + "@sentry/webpack-plugin": "^4.3.0", + "resolve": "1.22.8", + "rollup": "^4.35.0", + "stacktrace-parser": "^0.1.10" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0 || ^16.0.0-0" } - }, - "node_modules/@scure/bip32/node_modules/@noble/curves": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", - "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + }, + "node_modules/@sentry/nextjs/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "license": "MIT", "dependencies": { - "@noble/hashes": "1.4.0" + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@scure/bip32/node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" + "node_modules/@sentry/node": { + "version": "10.25.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.25.0.tgz", + "integrity": "sha512-++mugiYF8X7CLtpymGN3N4J40SvQVIsVa6K7pURhooT4eX1QXYOBJSaOqvOXk5GN4qed5wETHNBkZuXSO0RARQ==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^2.1.0", + "@opentelemetry/core": "^2.1.0", + "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/instrumentation-amqplib": "0.51.0", + "@opentelemetry/instrumentation-connect": "0.48.0", + "@opentelemetry/instrumentation-dataloader": "0.22.0", + "@opentelemetry/instrumentation-express": "0.53.0", + "@opentelemetry/instrumentation-fs": "0.24.0", + "@opentelemetry/instrumentation-generic-pool": "0.48.0", + "@opentelemetry/instrumentation-graphql": "0.52.0", + "@opentelemetry/instrumentation-hapi": "0.51.0", + "@opentelemetry/instrumentation-http": "0.204.0", + "@opentelemetry/instrumentation-ioredis": "0.52.0", + "@opentelemetry/instrumentation-kafkajs": "0.14.0", + "@opentelemetry/instrumentation-knex": "0.49.0", + "@opentelemetry/instrumentation-koa": "0.52.0", + "@opentelemetry/instrumentation-lru-memoizer": "0.49.0", + "@opentelemetry/instrumentation-mongodb": "0.57.0", + "@opentelemetry/instrumentation-mongoose": "0.51.0", + "@opentelemetry/instrumentation-mysql": "0.50.0", + "@opentelemetry/instrumentation-mysql2": "0.51.0", + "@opentelemetry/instrumentation-pg": "0.57.0", + "@opentelemetry/instrumentation-redis": "0.53.0", + "@opentelemetry/instrumentation-tedious": "0.23.0", + "@opentelemetry/instrumentation-undici": "0.15.0", + "@opentelemetry/resources": "^2.1.0", + "@opentelemetry/sdk-trace-base": "^2.1.0", + "@opentelemetry/semantic-conventions": "^1.37.0", + "@prisma/instrumentation": "6.15.0", + "@sentry/core": "10.25.0", + "@sentry/node-core": "10.25.0", + "@sentry/opentelemetry": "10.25.0", + "import-in-the-middle": "^1.14.2", + "minimatch": "^9.0.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@scure/bip39": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", - "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "node_modules/@sentry/node-core": { + "version": "10.25.0", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.25.0.tgz", + "integrity": "sha512-Hk0s7r9pkotZ1yfUc9+XX0ALDQ/bjaYsWF23O2q8Yfc4m8NcQio54ztAmdI+Yf+YiHLpt0x9Hlgwpl3AaRvwIA==", "license": "MIT", "dependencies": { - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" + "@apm-js-collab/tracing-hooks": "^0.3.1", + "@sentry/core": "10.25.0", + "@sentry/opentelemetry": "10.25.0", + "import-in-the-middle": "^1.14.2" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/instrumentation": ">=0.57.1 <1", + "@opentelemetry/resources": "^1.30.1 || ^2.1.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", + "@opentelemetry/semantic-conventions": "^1.37.0" } }, - "node_modules/@scure/bip39/node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "node_modules/@sentry/node/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/@scure/starknet": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/starknet/-/starknet-1.1.0.tgz", - "integrity": "sha512-83g3M6Ix2qRsPN4wqLDqiRZ2GBNbjVWfboJE/9UjfG+MHr6oDSu/CWgy8hsBSJejr09DkkL+l0Ze4KVrlCIdtQ==", - "license": "MIT", + "node_modules/@sentry/node/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", "dependencies": { - "@noble/curves": "~1.7.0", - "@noble/hashes": "~1.6.0" + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@scure/starknet/node_modules/@noble/curves": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz", - "integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==", + "node_modules/@sentry/opentelemetry": { + "version": "10.25.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.25.0.tgz", + "integrity": "sha512-AWCRzUIzvI+0RHXTmGvVx+MUtyyjwmC6F6d6XCnWhBKWGO52I+ucz1X8INIZxCrK05dpviFpeLZy+pzfgw892g==", "license": "MIT", "dependencies": { - "@noble/hashes": "1.6.0" + "@sentry/core": "10.25.0" }, "engines": { - "node": "^14.21.3 || >=16" + "node": ">=18" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", + "@opentelemetry/semantic-conventions": "^1.37.0" } }, - "node_modules/@scure/starknet/node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", - "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==", + "node_modules/@sentry/react": { + "version": "10.25.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.25.0.tgz", + "integrity": "sha512-LBQHgyPAFzuy99mEJF8ZF2AOxxJiGAmtu10eQhglhFgfJsU7JJVsee0h+vTSmvHMDtFrIwhZi3i1X5snZ/kzoA==", "license": "MIT", + "dependencies": { + "@sentry/browser": "10.25.0", + "@sentry/core": "10.25.0", + "hoist-non-react-statics": "^3.3.2" + }, "engines": { - "node": "^14.21.3 || >=16" + "node": ">=18" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, - "node_modules/@scure/starknet/node_modules/@noble/hashes": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz", - "integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==", + "node_modules/@sentry/vercel-edge": { + "version": "10.25.0", + "resolved": "https://registry.npmjs.org/@sentry/vercel-edge/-/vercel-edge-10.25.0.tgz", + "integrity": "sha512-TncKAvGo6YpFDKnf2k4l9Bdq8ZVdW+AlUClAloXs06sMLDZk9NDAmphT7lHdspx9kTsW/hTs29qEjk5H1AIZ8g==", "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/resources": "^2.1.0", + "@sentry/core": "10.25.0" + }, "engines": { - "node": "^14.21.3 || >=16" + "node": ">=18" + } + }, + "node_modules/@sentry/webpack-plugin": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-4.6.0.tgz", + "integrity": "sha512-i9Yy2kXCbFKlRST09fV1HsI0naJAfeXxoiUPyh5iCgSo2w7ZwEUlk0tJhupnHZzfSa3OSg01+vVNeeyLYM4tdA==", + "license": "MIT", + "dependencies": { + "@sentry/bundler-plugin-core": "4.6.0", + "unplugin": "1.0.1", + "uuid": "^9.0.0" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "webpack": ">=4.40.0" } }, "node_modules/@simplewebauthn/browser": { @@ -5889,18 +7611,38 @@ "@types/ms": "*" } }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -5916,6 +7658,15 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, + "node_modules/@types/mysql": { + "version": "2.15.27", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", + "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.19.21", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz", @@ -5945,6 +7696,26 @@ "@types/node": "*" } }, + "node_modules/@types/pg": { + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", + "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/pg-pool": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz", + "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==", + "license": "MIT", + "dependencies": { + "@types/pg": "*" + } + }, "node_modules/@types/qrcode": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", @@ -5984,12 +7755,27 @@ "@types/node": "*" } }, + "node_modules/@types/shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", + "license": "MIT" + }, "node_modules/@types/stylis": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", "license": "MIT" }, + "node_modules/@types/tedious": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", + "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/tiny-secp256k1": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/tiny-secp256k1/-/tiny-secp256k1-2.0.0.tgz", @@ -9121,6 +10907,181 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0", + "peer": true + }, "node_modules/abi-wan-kanabi": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/abi-wan-kanabi/-/abi-wan-kanabi-2.2.4.tgz", @@ -9235,7 +11196,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -9244,6 +11204,28 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -9273,6 +11255,18 @@ "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", "license": "MIT" }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/agentkeepalive": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", @@ -9292,16 +11286,58 @@ "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, "node_modules/alchemy-sdk": { "version": "3.6.5", "resolved": "https://registry.npmjs.org/alchemy-sdk/-/alchemy-sdk-3.6.5.tgz", @@ -9725,7 +11761,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base-x": { @@ -9754,6 +11789,15 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz", + "integrity": "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", @@ -9800,7 +11844,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9929,7 +11972,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -9958,6 +12000,39 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/bs58": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", @@ -10022,6 +12097,13 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT", + "peer": true + }, "node_modules/buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", @@ -10117,9 +12199,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001750", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", - "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", + "version": "1.0.30001755", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", + "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", "funding": [ { "type": "opencollective", @@ -10183,7 +12265,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -10208,7 +12289,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -10227,6 +12307,16 @@ "node": ">=18" } }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, "node_modules/cipher-base": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", @@ -10241,6 +12331,12 @@ "node": ">= 0.10" } }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT" + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -10327,6 +12423,12 @@ "node": ">=20" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -10334,6 +12436,12 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, "node_modules/cookie-es": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", @@ -10809,6 +12917,12 @@ "tweetnacl": "1.0.3" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.254", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz", + "integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==", + "license": "ISC" + }, "node_modules/elliptic": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", @@ -10865,7 +12979,6 @@ "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -10990,6 +13103,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT", + "peer": true + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -11600,7 +13720,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -11613,12 +13732,17 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -11990,7 +14114,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -12064,6 +14187,23 @@ "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/fast-xml-parser": { "version": "5.2.5", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", @@ -12116,7 +14256,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -12138,7 +14277,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -12223,6 +14361,12 @@ "node": ">= 6" } }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" + }, "node_modules/framer-motion": { "version": "12.23.24", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", @@ -12264,11 +14408,16 @@ "node": ">=12" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -12329,6 +14478,15 @@ "node": ">= 0.4" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -12426,6 +14584,24 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -12439,6 +14615,46 @@ "node": ">=10.13.0" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", + "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -12679,6 +14895,28 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -12760,6 +14998,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-in-the-middle": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz", + "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.14.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -12858,7 +15108,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -12916,7 +15165,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -12967,7 +15215,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13022,7 +15269,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -13071,7 +15317,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -13094,6 +15339,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -13254,7 +15508,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/isomorphic-fetch": { @@ -13377,6 +15630,47 @@ } } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -13439,6 +15733,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -13446,6 +15752,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT", + "peer": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -13911,11 +16224,24 @@ "@types/trusted-types": "^2.0.2" } }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -14019,7 +16345,6 @@ "version": "0.30.19", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -14063,6 +16388,13 @@ "safe-buffer": "^5.1.2" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT", + "peer": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -14152,7 +16484,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -14191,6 +16522,12 @@ } } }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, "node_modules/motion-dom": { "version": "12.23.23", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", @@ -14259,6 +16596,13 @@ "dev": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT", + "peer": true + }, "node_modules/next": { "version": "15.5.2", "resolved": "https://registry.npmjs.org/next/-/next-15.5.2.tgz", @@ -14418,6 +16762,12 @@ "integrity": "sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==", "license": "MIT" }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, "node_modules/nodemailer": { "version": "7.0.10", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz", @@ -14721,7 +17071,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -14737,7 +17086,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -14800,9 +17148,24 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pbkdf2": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", @@ -14820,6 +17183,37 @@ "node": ">= 0.10" } }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -15007,6 +17401,45 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/preact": { "version": "10.24.2", "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.2.tgz", @@ -15048,6 +17481,15 @@ "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", "license": "MIT" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -15353,7 +17795,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/react-remove-scroll": { @@ -15450,7 +17891,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -15530,6 +17970,30 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -15540,7 +18004,6 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -15613,6 +18076,47 @@ "rlp": "bin/rlp" } }, + "node_modules/rollup": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", + "fsevents": "~2.3.2" + } + }, "node_modules/rome": { "version": "12.1.3", "resolved": "https://registry.npmjs.org/rome/-/rome-12.1.3.tgz", @@ -15812,6 +18316,63 @@ "loose-envify": "^1.1.0" } }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, "node_modules/scrypt-js": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", @@ -15863,6 +18424,16 @@ "node": ">=10" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -16021,6 +18592,12 @@ "node": ">=8" } }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", + "license": "BSD-2-Clause" + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -16144,6 +18721,16 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -16153,6 +18740,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/split-on-first": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", @@ -16178,6 +18776,18 @@ "dev": true, "license": "MIT" }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/starknet": { "version": "7.6.4", "resolved": "https://registry.npmjs.org/starknet/-/starknet-7.6.4.tgz", @@ -16616,7 +19226,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -16652,7 +19261,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -16679,6 +19287,67 @@ "node": ">=18" } }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT", + "peer": true + }, "node_modules/text-encoding-utf-8": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", @@ -16792,7 +19461,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -16940,6 +19608,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -17142,6 +19819,18 @@ "node": ">= 10.0.0" } }, + "node_modules/unplugin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz", + "integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.8.1", + "chokidar": "^3.5.3", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.5.0" + } + }, "node_modules/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", @@ -17177,6 +19866,36 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -17508,12 +20227,114 @@ } } }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/webpack": { + "version": "5.102.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", + "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", + "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", + "license": "MIT" + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/websocket": { "version": "1.0.35", "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz", @@ -17566,7 +20387,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -17752,6 +20572,15 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -17822,7 +20651,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" From 3d7ab9d2a7df6b6ea9e074c541bfe9b18f171ff0 Mon Sep 17 00:00:00 2001 From: tali-creator Date: Tue, 18 Nov 2025 18:38:18 +0100 Subject: [PATCH 2/9] fixed --- components/dashboard/mobile-bottom-nav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/dashboard/mobile-bottom-nav.tsx b/components/dashboard/mobile-bottom-nav.tsx index d1b6f9c..716442a 100644 --- a/components/dashboard/mobile-bottom-nav.tsx +++ b/components/dashboard/mobile-bottom-nav.tsx @@ -22,7 +22,7 @@ export function MobileBottomNav({ activeTab, setActiveTab }: MobileBottomNavProp return (
-
+
{navItems.map((item, index) => ( - )} -
-
- - { - setFormData((prev) => ({ - ...prev, - phoneNo: e.target.value, - })); - setMeterVerified(false); - setMeterVerificationMessage(""); - }} - placeholder="08123456789" - type="tel" - className="flex-1 p-4 rounded-2xl border-none outline-none " - /> -
- {meterVerificationMessage && ( -
- {meterVerificationMessage} -
- )} -
- )} +
+ )} - {errorMessage && ( -
- - {errorMessage} -
- )} + {errorMessage && ( +
+ + {errorMessage} +
+ )} -
+ + Continue + + ); case 2: return ( -
-

Select Payment Method

-

- Choose your preferred cryptocurrency -

+
+ + + +

Select Payment Method

- - - {merchantFallback && ( -
- No merchant wallet configured for {selectedToken.toUpperCase()}. - Using your connected wallet address for testing only. Do not use - in production. To fix permanently, set the environment variable - NEXT_PUBLIC_{selectedToken.toUpperCase()}_WALLET - or run in the console: - {`window.__VELO_MERCHANT_WALLETS = ${JSON.stringify( - { [selectedToken]: currentWalletAddress } - )}`} -
- )} - - {formData.expectedAmount && ( -
-

Payment Details

-
-
- Amount (NGN): - - ₦{parseFloat(formData.amount).toLocaleString()} - -
-
- - Amount ({formData.expectedAmount.cryptoCurrency}): - - - {formData.expectedAmount.cryptoAmount.toFixed(8)} - -
- {formData.expectedAmount.planDetails && ( - <> -
- Plan: - - {formData.expectedAmount.planDetails.name} - -
- {formData.expectedAmount.planDetails.validity && ( -
- Validity: - - {formData.expectedAmount.planDetails.validity} - -
- )} - - )} +
+
+
+

Amount

+

₦{parseInt(formData.amount || "0").toLocaleString()}

+
+
+

Provider

+

{providers.find(p => p.serviceID === formData.service_id)?.name}

- )} +
- {errorMessage && ( -
- - {errorMessage} -
- )} +
+ {["Ethereum", "Bitcoin", "USDT", "Solana"].map((crypto) => ( + setSelectedToken(crypto.toLowerCase())} + className={`p-6 rounded-2xl transition-all ${ + selectedToken === crypto.toLowerCase() + ? "bg-gradient-to-br from-cyan-500 to-blue-500 text-white shadow-lg" + : "bg-white/5 text-gray-300 hover:bg-white/10 border border-white/10" + }`} + > +
+
{crypto[0]}
+
{crypto}
+
+
+ ))} +
-
); @@ -1070,272 +1006,151 @@ export default function Purchase({ type }: PurchaseProps) { case 3: return ( -
-
-

- Summary -

-

- Review your purchase -

-
- -
-
- - ₦{parseInt(formData.amount || "0").toLocaleString()} - - - - - - {formData.expectedAmount?.cryptoAmount.toFixed(8)}{" "} - {formData.expectedAmount?.cryptoCurrency} - -
+
+ + + +

Transaction Summary

+
-
- - p.serviceID === formData.service_id) - ?.name || "" - } - /> - {type === "data" && formData.dataplan && ( - - )} - {type === "electricity" && ( - - )} - - -
+
+
+ Product + {type}
- - {errorMessage && ( -
- - {errorMessage} -
- )} - -
-
- setShowPinDialog(false)} - onPinComplete={handleSendWithPin} - isLoading={isSending} - title="Authorize Transaction" - description="Enter your transaction PIN to confirm this purchase" - /> +
+ + Back + + + Confirm & Pay + + +
); case 4: return ( -
- - {success ? ( - - ) : ( - - )} - - -
-

- {success ? "Transaction Successful" : "Transaction Failed"} -

-

- {success - ? "Your transaction has been processed successfully" - : "Something went wrong. Please try again."} -

- -
- Amount Sent - - ₦{parseInt(formData.amount || "0").toLocaleString()} - -
- -
-
- Beneficiary -
-
-
-
- {formData.transactionData?.providerLogo ? ( - provider - ) : ( -
- {providers.find((p) => p.serviceID === formData.service_id)?.name?.substring(0,3) || "P"} -
- )} -
-
-

- {type === "electricity" - ? formData.customer_id - : `234${formData.customer_id}`} -

-

- { - providers.find( - (p) => p.serviceID === formData.service_id - )?.name - } -

-
-
- -
+ + {success ? ( +
+
-
- - {success && formData.transactionData && ( -
- - - - - - - {formData.transactionData.meterToken && ( -
-

- Meter Token -

-

- {formData.transactionData.meterToken} -

-
- )} + ) : ( +
+
)} + + +
+

+ {success ? "Transaction Successful!" : "Transaction Failed"} +

+

+ {success + ? "Your transaction has been processed successfully" + : "Something went wrong. Please try again."} +

+
- {!success && ( -
-

{errorMessage}

+ {success && ( +
+
+ Amount + ₦{parseInt(formData.amount || "0").toLocaleString()}
- )} -
+
+ Provider + {providers.find(p => p.serviceID === formData.service_id)?.name} +
+
+ Status + Completed +
+
+ )} -
-
+ + + New Purchase + -
-
+ {/* Confetti effect */} + {success && ( +
+ {[...Array(20)].map((_, i) => ( + + ))} +
+ )} ); @@ -1345,14 +1160,96 @@ export default function Purchase({ type }: PurchaseProps) { }; return ( -
-
- {renderStep()} -
+
+ {/* PIN Dialog */} + {showPinDialog && ( + setShowPinDialog(false)} + onSubmit={handleSendWithPin} + isLoading={isSending} + /> + )} + + {renderStep()}
); } +// PIN Dialog Component +function PinDialog({ + onClose, + onSubmit, + isLoading, +}: { + onClose: () => void; + onSubmit: (pin: string) => void; + isLoading: boolean; +}) { + const [pin, setPin] = useState(["", "", "", ""]); + const inputRefs = React.useRef<(HTMLInputElement | null)[]>([]); + + const handleChange = (index: number, value: string) => { + if (value.length > 1) return; + if (value && !/^\d$/.test(value)) return; + + const newPin = [...pin]; + newPin[index] = value; + setPin(newPin); + + if (value && index < 3) { + inputRefs.current[index + 1]?.focus(); + } + + if (newPin.every((digit) => digit !== "") && index === 3) { + onSubmit(newPin.join("")); + } + }; + + return ( + + e.stopPropagation()} + className="bg-gradient-to-br from-gray-900 to-gray-800 rounded-3xl p-8 max-w-md w-full border border-white/20" + > +

Enter PIN

+ +
+ {pin.map((digit, index) => ( + (inputRefs.current[index] = el)} + type="password" + inputMode="numeric" + maxLength={1} + value={digit} + onChange={(e) => handleChange(index, e.target.value)} + disabled={isLoading} + className="w-16 h-20 text-center bg-white/10 backdrop-blur-sm border-2 border-white/20 rounded-2xl text-white text-2xl focus:outline-none focus:border-purple-500 focus:ring-4 focus:ring-purple-500/30 transition-all" + /> + ))} +
+ + {isLoading && ( +
+ + Processing... +
+ )} +
+
+ ); +} + + // === MISSING SUB-COMPONENTS (Added) === function DataPlanSelect({ diff --git a/components/hooks/use-transaction-pin.ts b/components/hooks/use-transaction-pin.ts deleted file mode 100644 index 7713c06..0000000 --- a/components/hooks/use-transaction-pin.ts +++ /dev/null @@ -1,117 +0,0 @@ -// import { useCallback, useState } from 'react'; -// import { apiClient } from '@/lib/api-client'; -// import { toast } from 'sonner'; - -// interface TransactionPinHook { -// // State -// isPinDialogOpen: boolean; -// isProcessing: boolean; - -// // Methods -// requestTransactionPin: (transactionData: any) => Promise<{ -// success: boolean; -// message: string; -// data?: any; -// }>; -// openPinDialog: () => void; -// closePinDialog: () => void; -// handlePinSubmit: (pin: string) => Promise; -// } - -// export const useTransactionPin = (): TransactionPinHook => { -// const [isPinDialogOpen, setIsPinDialogOpen] = useState(false); -// const [isProcessing, setIsProcessing] = useState(false); -// const [pendingTransaction, setPendingTransaction] = useState(null); -// const [transactionResolver, setTransactionResolver] = useState<((value: any) => void) | null>(null); - -// // Request PIN for transaction (returns a promise) -// const requestTransactionPin = useCallback((transactionData: any): Promise => { -// return new Promise((resolve) => { -// setPendingTransaction(transactionData); -// setTransactionResolver(() => resolve); -// setIsPinDialogOpen(true); -// }); -// }, []); - -// // Handle PIN submission and transaction execution -// const handlePinSubmit = useCallback(async (pin: string) => { -// if (!pendingTransaction) return; - -// setIsProcessing(true); - -// try { -// // Step 1: Verify the PIN first -// const pinVerification = await apiClient.TransactionPin(pin); - -// if (!pinVerification.success) { -// throw new Error(pinVerification.message || 'Invalid PIN'); -// } - -// // Step 2: If PIN is valid, execute the transaction with PIN included -// let transactionResult; - -// switch (pendingTransaction.type) { -// case 'send': -// // Include PIN in the transaction data for backend signing -// const transactionWithPin = { -// ...pendingTransaction, -// transactionPin: pin // ADD PIN to transaction data -// }; -// transactionResult = await apiClient.sendTransaction(transactionWithPin); -// break; -// case 'split-payment': -// transactionResult = await apiClient.executeSplitPayment(pin); -// break; -// default: -// throw new Error('Unknown transaction type'); -// } - -// // Step 3: Return success result -// if (transactionResolver) { -// transactionResolver({ -// success: true, -// message: 'Transaction completed successfully', -// data: transactionResult -// }); -// } - -// toast.success('Transaction completed successfully'); -// closePinDialog(); - -// } catch (error: any) { -// const errorMessage = error.message || 'Transaction failed'; - -// if (transactionResolver) { -// transactionResolver({ -// success: false, -// message: errorMessage -// }); -// } - -// toast.error(errorMessage); -// closePinDialog(); -// } finally { -// setIsProcessing(false); -// } -// }, [pendingTransaction, transactionResolver]); - -// const openPinDialog = useCallback(() => { -// setIsPinDialogOpen(true); -// }, []); - -// const closePinDialog = useCallback(() => { -// setIsPinDialogOpen(false); -// setPendingTransaction(null); -// setTransactionResolver(null); -// setIsProcessing(false); -// }, []); - -// return { -// isPinDialogOpen, -// isProcessing, -// requestTransactionPin, -// openPinDialog, -// closePinDialog, -// handlePinSubmit -// }; -// }; \ No newline at end of file diff --git a/components/hooks/useGetBalance.tsx b/components/hooks/useGetBalance.tsx deleted file mode 100644 index 75047e7..0000000 --- a/components/hooks/useGetBalance.tsx +++ /dev/null @@ -1,144 +0,0 @@ -// import { useEffect, useState } from "react"; -// import { useAccount } from "@starknet-react/core"; -// import { Contract, RpcProvider } from "starknet"; -// import { VELO_ABI as BALANCE_ABI } from "./useSwiftContract"; -// import { TOKEN_ADDRESSES as tokenAddress } from "autoswap-sdk"; -// // token addresses -// const TOKEN_ADDRESSES = { -// STRK: tokenAddress.STRK, -// USDC: tokenAddress.USDC, -// USDT: tokenAddress.USDT, -// }; - -// // Token decimals for formatting -// const TOKEN_DECIMALS = { -// [TOKEN_ADDRESSES.STRK]: 18, -// [TOKEN_ADDRESSES.USDC]: 6, -// [TOKEN_ADDRESSES.USDT]: 6, -// }; - -// // Central contract address for get_balance -// const BALANCE_CONTRACT_ADDRESS = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS; - -// type Balances = { -// STRK: string; -// USDC: string; -// USDT: string; -// }; - -// type UseGetBalanceReturn = { -// balances: Balances; -// loading: boolean; -// error: string | null; -// refetch: () => void; -// }; - -// export default function useGetBalance(): UseGetBalanceReturn { -// const { address } = useAccount(); -// const [balances, setBalances] = useState({ -// STRK: "0", -// USDC: "0", -// USDT: "0", -// }); -// const [loading, setLoading] = useState(false); -// const [error, setError] = useState(null); - -// const provider = new RpcProvider({ -// nodeUrl: "https://starknet-sepolia.public.blastapi.io", -// }); - -// const fetchBalances = async () => { -// if (!address) { -// setBalances({ -// STRK: "0", -// USDC: "0", -// USDT: "0", -// }); -// setError("No wallet connected"); -// setLoading(false); -// return; -// } - -// setLoading(true); -// setError(null); - -// try { -// if (!BALANCE_CONTRACT_ADDRESS) { -// throw new Error("Balance contract address is not defined"); -// } - -// const contract = new Contract( -// BALANCE_ABI, -// BALANCE_CONTRACT_ADDRESS, -// provider -// ); - -// const balancePromises = Object.keys(TOKEN_ADDRESSES).map((token) => -// contract -// .call("get_balance", [ -// address, -// TOKEN_ADDRESSES[token as keyof typeof TOKEN_ADDRESSES], -// ]) -// .catch((err: any) => { -// console.error(`Error fetching balance for ${token}:`, err); -// throw new Error(`Failed to fetch ${token} balance: ${err.message}`); -// }) -// ); - -// const results = await Promise.allSettled(balancePromises); - -// const newBalances: Balances = { -// STRK: "0", -// USDC: "0", -// USDT: "0", -// }; - -// results.forEach((result, index) => { -// const token = Object.keys(TOKEN_ADDRESSES)[index] as keyof Balances; -// const decimals = TOKEN_DECIMALS[TOKEN_ADDRESSES[token]]; - -// if (result.status === "fulfilled") { -// const balanceValue = -// typeof result.value === "object" && "balance" in result.value -// ? result.value.balance -// : Array.isArray(result.value) -// ? result.value[0] -// : result.value; - -// newBalances[token] = (Number(balanceValue) / 10 ** decimals).toFixed( -// 2 -// ); -// } else { -// console.error(`Failed to fetch ${token} balance:`, result.reason); -// setError((prev) => -// prev ? `${prev}; ${result.reason.message}` : result.reason.message -// ); -// newBalances[token] = "0"; -// } -// }); - -// setBalances(newBalances); -// } catch (err: any) { -// console.error("Error fetching balances:", err); -// setError(`Failed to fetch balances: ${err.message || "Unknown error"}`); -// setBalances({ -// STRK: "0", -// USDC: "0", -// USDT: "0", -// }); -// } finally { -// setLoading(false); -// } -// }; - -// useEffect(() => { -// fetchBalances(); -// }, [address]); - -// return { -// balances, -// loading, -// error, -// refetch: fetchBalances, -// }; -// } diff --git a/components/hooks/useTotalBalance.tsx b/components/hooks/useTotalBalance.tsx deleted file mode 100644 index 861aa39..0000000 --- a/components/hooks/useTotalBalance.tsx +++ /dev/null @@ -1,100 +0,0 @@ -// // hooks/useTotalBalance.ts (Improved) -// import { useEffect, useState, useCallback } from "react"; -// import { useAuth } from "../context/AuthContext"; -// import useExchangeRates from "./useExchangeRate"; - -// interface BalanceBreakdown { -// chain: string; -// symbol: string; -// balance: number; -// ngnValue: number; -// rate: number | null; -// } - -// interface BalanceSummary { -// totalNGN: number; -// breakdown: BalanceBreakdown[]; -// } - -// export const useTotalBalance = () => { -// const { getWalletBalances, token } = useAuth(); -// const { rates } = useExchangeRates(); -// const [balanceSummary, setBalanceSummary] = useState({ -// totalNGN: 0, -// breakdown: [] -// }); -// const [loading, setLoading] = useState(true); -// const [error, setError] = useState(null); - -// // Map symbols to rate keys -// const getRateKey = useCallback((symbol: string): keyof typeof rates => { -// const symbolMap: { [key: string]: keyof typeof rates } = { -// 'ETH': 'ETH', -// 'BTC': 'BTC', -// 'SOL': 'SOL', -// 'STRK': 'STRK', -// 'USDT': 'USDT', -// 'USDC': 'USDC', -// 'DOT': 'DOT', -// 'XLM': 'XML' -// }; -// return symbolMap[symbol] || 'USDT'; -// }, []); - -// const calculateTotalBalance = useCallback(async () => { -// if (!token) { -// setError("Authentication required"); -// setLoading(false); -// return; -// } - -// try { -// setLoading(true); -// setError(null); - -// const balances = await getWalletBalances(); - -// const breakdown = balances.map(balance => { -// const rateKey = getRateKey(balance.symbol); -// const rate = rates[rateKey] || 1; -// const numericBalance = parseFloat(balance.balance) || 0; -// const ngnValue = numericBalance * rate; - -// return { -// chain: balance.chain, -// symbol: balance.symbol, -// balance: numericBalance, -// ngnValue, -// rate -// }; -// }); - -// const totalNGN = breakdown.reduce((sum, item) => sum + item.ngnValue, 0); - -// setBalanceSummary({ -// totalNGN, -// breakdown -// }); - -// } catch (err) { -// setError(err instanceof Error ? err.message : 'Failed to calculate balance'); -// console.error('Error calculating total balance:', err); -// } finally { -// setLoading(false); -// } -// }, [token, getWalletBalances, rates, getRateKey]); - -// useEffect(() => { -// if (token && rates.ETH !== null) { -// calculateTotalBalance(); -// } -// }, [token, rates.ETH, calculateTotalBalance]); - -// return { -// totalBalance: balanceSummary.totalNGN, -// breakdown: balanceSummary.breakdown, -// loading, -// error, -// refetch: calculateTotalBalance -// }; -// }; \ No newline at end of file diff --git a/components/providers/ethereum-provider.ts b/components/providers/ethereum-provider.ts deleted file mode 100644 index 605ab1d..0000000 --- a/components/providers/ethereum-provider.ts +++ /dev/null @@ -1,223 +0,0 @@ -// providers/ethereum-provider.ts -import { ethers } from "ethers"; -import { BlockchainProvider, WalletMonitorResult, Transaction } from "@/types/multi-chain"; - -const KNOWN_ERC20_TOKENS: { [address: string]: { symbol: string; decimals: number } } = { - // Native ETH (special zero address) - "0x0000000000000000000000000000000000000000": { symbol: "ETH", decimals: 18 }, - - // Sepolia testnet tokens (real addresses) - "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9": { symbol: "WETH", decimals: 18 }, - "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238": { symbol: "USDC", decimals: 6 }, - "0xE72f3E105e475D7Db3a003FfA377aFAe9d2B7A15": { symbol: "DAI", decimals: 18 }, -}; - - -export class EthereumProvider implements BlockchainProvider { - name = "ethereum"; - chainId: string; - private provider: ethers.JsonRpcProvider; - - constructor(nodeUrl: string, chainId: string) { - this.provider = new ethers.JsonRpcProvider(nodeUrl); - this.chainId = chainId; - } - - async monitorWalletTransactions( - walletAddress: string, - fromBlock?: number - ): Promise { - const normalizedAddress = this.normalizeAddress(walletAddress); - const currentBlock = await this.provider.getBlockNumber(); - const scanFromBlock = fromBlock || Math.max(0, currentBlock - 10000); - - const transactions: Transaction[] = []; - - // Monitor ETH transfers - const ethTransfers = await this.getEthTransfers(normalizedAddress, scanFromBlock, currentBlock); - transactions.push(...ethTransfers); - - // Monitor ERC20 transfers - const erc20Transfers = await this.getERC20Transfers(normalizedAddress, scanFromBlock, currentBlock); - transactions.push(...erc20Transfers); - - return { - status: "success", - walletAddress: normalizedAddress, - transactions: transactions.sort((a, b) => b.block - a.block), - totalTransactions: transactions.length, - scannedBlocks: { from: scanFromBlock, to: currentBlock } - }; - } - - private async getEthTransfers( - walletAddress: string, - fromBlock: number, - toBlock: number - ): Promise { - const transactions: Transaction[] = []; - - try { - // Get ETH transfers by scanning transaction receipts - const filter = { - fromBlock: fromBlock, - toBlock: toBlock, - to: walletAddress - }; - - console.log(filter) - - // This is a simplified approach - in production, you might want to use a more efficient method - // like The Graph or specialized indexing services - const logs = await this.provider.getLogs({ - fromBlock: fromBlock, - toBlock: toBlock, - }); - - console.log(logs) - // Process blocks in chunks to avoid timeout - for (let blockNum = fromBlock; blockNum <= toBlock; blockNum += 1000) { - const endBlock = Math.min(blockNum + 999, toBlock); - - const block = await this.provider.getBlock(blockNum, true); - if (!block || !block.transactions) continue; - console.log(endBlock) - - for (const txHash of block.transactions) { - try { - const receipt = await this.provider.getTransactionReceipt(txHash); - const tx = await this.provider.getTransaction(txHash); - - if (receipt && tx && receipt.to && this.normalizeAddress(receipt.to) === walletAddress) { - const block = await this.provider.getBlock(receipt.blockNumber); - - transactions.push({ - hash: txHash, - block: receipt.blockNumber, - timestamp: block?.timestamp || Math.floor(Date.now() / 1000), - confirmations: toBlock - receipt.blockNumber, - from: tx.from, - to: receipt.to, - amount: ethers.formatEther(tx.value), - tokenAddress: "0x0000000000000000000000000000000000000000", - tokenSymbol: "ETH", - tokenDecimals: 18, - type: "ETH_TRANSFER" - }); - } - } catch (error) { - console.warn(`Failed to process transaction ${txHash}:`, error); - } - } - } - } catch (error) { - console.error("Error fetching ETH transfers:", error); - } - - return transactions; - } - - private async getERC20Transfers( - walletAddress: string, - fromBlock: number, - toBlock: number - ): Promise { - const transactions: Transaction[] = []; - - try { - // ERC20 Transfer event signature - const transferTopic = ethers.id("Transfer(address,address,uint256)"); - - // Get all Transfer events where the recipient is our wallet - const logs = await this.provider.getLogs({ - fromBlock: fromBlock, - toBlock: toBlock, - topics: [ - transferTopic, - null, // from - ethers.zeroPadValue(walletAddress, 32) // to - ] - }); - - for (const log of logs) { - try { - const block = await this.provider.getBlock(log.blockNumber); - const tx = await this.provider.getTransaction(log.transactionHash); - console.log(tx) - // Parse transfer parameters from event data - const from = ethers.getAddress(ethers.dataSlice(log.topics[1], 12)); - const to = ethers.getAddress(ethers.dataSlice(log.topics[2], 12)); - const amount = ethers.getBigInt(log.data); - - const tokenInfo = await this.getTokenInfo(log.address); - - transactions.push({ - hash: log.transactionHash, - block: log.blockNumber, - timestamp: block?.timestamp || Math.floor(Date.now() / 1000), - confirmations: toBlock - log.blockNumber, - from, - to, - amount: amount.toString(), - tokenAddress: log.address, - tokenSymbol: tokenInfo?.symbol, - tokenDecimals: tokenInfo?.decimals, - type: "ERC20_TRANSFER" - }); - } catch (error) { - console.warn(`Failed to process ERC20 transfer in tx ${log.transactionHash}:`, error); - } - } - } catch (error) { - console.error("Error fetching ERC20 transfers:", error); - } - - return transactions; - } - - private async getTokenInfo(tokenAddress: string): Promise<{ symbol: string; decimals: number } | null> { - const normalizedAddr = this.normalizeAddress(tokenAddress); - - // Check known tokens first - if (KNOWN_ERC20_TOKENS[normalizedAddr]) { - return KNOWN_ERC20_TOKENS[normalizedAddr]; - } - - try { - // Create minimal ERC20 interface - const erc20Interface = new ethers.Interface([ - "function symbol() view returns (string)", - "function decimals() view returns (uint8)" - ]); - - const contract = new ethers.Contract(tokenAddress, erc20Interface, this.provider); - - const [symbol, decimals] = await Promise.all([ - contract.symbol().catch(() => "UNKNOWN"), - contract.decimals().catch(() => 18) - ]); - - return { symbol, decimals }; - } catch (error) { - console.warn(`Could not fetch token info for ${tokenAddress}:`, error); - return { symbol: "UNKNOWN", decimals: 18 }; - } - } - - async testConnection(): Promise { - try { - await this.provider.getBlockNumber(); - return true; - } catch { - return false; - } - } - - normalizeAddress(address: string): string { - return ethers.getAddress(address); - } - - isValidAddress(address: string): boolean { - return ethers.isAddress(address); - } -} \ No newline at end of file diff --git a/components/providers/solana-provider.ts b/components/providers/solana-provider.ts deleted file mode 100644 index a849e36..0000000 --- a/components/providers/solana-provider.ts +++ /dev/null @@ -1,138 +0,0 @@ -// providers/solana-provider-simple.ts -import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js"; -import { BlockchainProvider, WalletMonitorResult, Transaction } from "@/types/multi-chain"; - -export class SolanaProvider implements BlockchainProvider { - name = "solana"; - chainId = "solana-mainnet"; - private connection: Connection; - - constructor(nodeUrl: string) { - this.connection = new Connection(nodeUrl); - } - - async monitorWalletTransactions( - walletAddress: string, - - ): Promise { - const publicKey = new PublicKey(walletAddress); - const transactions: Transaction[] = []; - - try { - // Get confirmed signatures (more reliable than getSignaturesForAddress) - const signatures = await this.connection.getConfirmedSignaturesForAddress2( - publicKey, - { limit: 50 } - ); - - for (const signatureInfo of signatures) { - try { - // Use getParsedTransaction which handles versioned transactions better - const transaction = await this.connection.getParsedTransaction( - signatureInfo.signature, - { maxSupportedTransactionVersion: 0 } - ); - - if (transaction && transaction.meta) { - const solTransfers = await this.parseTransactionTransfers(transaction, publicKey); - transactions.push(...solTransfers); - } - } catch (error) { - console.warn(`Failed to process Solana transaction ${signatureInfo.signature}:`, error); - } - } - } catch (error) { - console.error("Error monitoring Solana wallet:", error); - return { - status: "error", - walletAddress, - transactions: [], - totalTransactions: 0, - scannedBlocks: { from: 0, to: 0 }, - error: "Failed to monitor Solana wallet", - details: error instanceof Error ? error.message : "Unknown error" - }; - } - - return { - status: "success", - walletAddress: walletAddress, - transactions: transactions.sort((a, b) => b.block - a.block), - totalTransactions: transactions.length, - scannedBlocks: { from: 0, to: 0 } - }; - } - - private async parseTransactionTransfers( - transaction: any, // Using any to avoid type complexity - walletAddress: PublicKey - ): Promise { - const transfers: Transaction[] = []; - - if (!transaction.meta) return transfers; - - try { - // Simple balance change detection - const preBalances = transaction.meta.preBalances; - const postBalances = transaction.meta.postBalances; - - const accountKeys = transaction.transaction.message.accountKeys.map( - (acc: any) => new PublicKey(acc.pubkey) - ); - - for (let i = 0; i < accountKeys.length; i++) { - const account = accountKeys[i]; - - if (account.equals(walletAddress)) { - const balanceChange = postBalances[i] - preBalances[i]; - - if (balanceChange > 0) { - transfers.push({ - hash: transaction.transaction.signatures[0], - block: transaction.slot, - timestamp: transaction.blockTime || Math.floor(Date.now() / 1000), - confirmations: 0, - from: "unknown", // Simplified - you'd need complex analysis to find the sender - to: walletAddress.toBase58(), - amount: (balanceChange / LAMPORTS_PER_SOL).toString(), - tokenAddress: "SOL", - tokenSymbol: "SOL", - tokenDecimals: 9, - type: "SOL_TRANSFER" - }); - } - } - } - } catch (error) { - console.warn("Failed to parse transaction transfers:", error); - } - - return transfers; - } - - async testConnection(): Promise { - try { - await this.connection.getEpochInfo(); - return true; - } catch { - return false; - } - } - - normalizeAddress(address: string): string { - try { - return new PublicKey(address).toBase58(); - } catch { - return address; - } - } - - isValidAddress(address: string): boolean { - try { - new PublicKey(address); - return true; - } catch { - return false; - } - } -} \ No newline at end of file diff --git a/lib/api-client.ts b/lib/api-client.ts index 673044e..d412738 100644 --- a/lib/api-client.ts +++ b/lib/api-client.ts @@ -53,12 +53,7 @@ import { // talks to your local backend (port 5500) while NEXT_PUBLIC_API_URL can // remain pointed to the live backend. In production we always use // NEXT_PUBLIC_API_URL. -const url = (() => { - // Always resolve to NEXT_PUBLIC_API_URL. We prefer using the explicit - // public backend URL configured in environment rather than a separate - // DEV_BACKEND_API_URL to keep runtime behavior consistent across builds. - return (process.env.NEXT_PUBLIC_API_URL as string) || ""; -})(); +const url = "https://velo-node-backend.onrender.com"; // Service types export interface SupportedNetwork { diff --git a/lib/api.ts b/lib/api.ts index 04f9d88..1eb042c 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -7,7 +7,7 @@ import { tokenManager } from "@/components/lib/api"; const API_BASE_URL = (process.env.NEXT_PUBLIC_BACKEND_URL as string) || (process.env.NEXT_PUBLIC_API_URL as string) || - ""; + "https://velo-node-backend.onrender.com"; export async function getStats(): Promise { const token = tokenManager.getToken(); diff --git a/package-lock.json b/package-lock.json index d408bab..8b15aa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5388,7 +5388,6 @@ "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", @@ -5933,7 +5932,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz", "integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5974,7 +5972,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -5985,7 +5982,6 @@ "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6082,7 +6078,6 @@ "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", @@ -9288,7 +9283,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -10015,7 +10009,6 @@ "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", "license": "MIT", - "peer": true, "dependencies": { "base-x": "^5.0.0" } @@ -10087,7 +10080,6 @@ "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -10540,8 +10532,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/d": { "version": "1.0.2", @@ -11205,7 +11196,6 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -11380,7 +11370,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -14330,7 +14319,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.2.tgz", "integrity": "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.5.2", "@swc/helpers": "0.5.15", @@ -14531,7 +14519,6 @@ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz", "integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==", "license": "MIT-0", - "peer": true, "engines": { "node": ">=6.0.0" } @@ -15178,7 +15165,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.2.tgz", "integrity": "sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -15484,7 +15470,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -15510,7 +15495,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -16370,7 +16354,6 @@ "resolved": "https://registry.npmjs.org/starknet/-/starknet-7.6.4.tgz", "integrity": "sha512-FB20IaLCDbh/XomkB+19f5jmNxG+RzNdRO7QUhm7nfH81UPIt2C/MyWAlHCYkbv2wznSEb73wpxbp9tytokTgQ==", "license": "MIT", - "peer": true, "dependencies": { "@noble/curves": "1.7.0", "@noble/hashes": "1.6.0", @@ -16949,7 +16932,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -17226,7 +17208,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17431,7 +17412,6 @@ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -17442,7 +17422,6 @@ "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -17501,7 +17480,6 @@ "resolved": "https://registry.npmjs.org/valtio/-/valtio-2.1.7.tgz", "integrity": "sha512-DwJhCDpujuQuKdJ2H84VbTjEJJteaSmqsuUltsfbfdbotVfNeTE4K/qc/Wi57I9x8/2ed4JNdjEna7O6PfavRg==", "license": "MIT", - "peer": true, "dependencies": { "proxy-compare": "^3.0.1" }, @@ -17550,7 +17528,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", @@ -17930,7 +17907,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/styles/service-styles.css b/styles/service-styles.css new file mode 100644 index 0000000..356550b --- /dev/null +++ b/styles/service-styles.css @@ -0,0 +1,191 @@ +@import "tailwindcss"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --font-size: 16px; + --background: #ffffff; + --foreground: oklch(0.145 0 0); + --card: #ffffff; + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: #030213; + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.95 0.0058 264.53); + --secondary-foreground: #030213; + --muted: #ececf0; + --muted-foreground: #717182; + --accent: #e9ebef; + --accent-foreground: #030213; + --destructive: #d4183d; + --destructive-foreground: #ffffff; + --border: rgba(0, 0, 0, 0.1); + --input: transparent; + --input-background: #f3f3f5; + --switch-background: #cbced4; + --font-weight-medium: 500; + --font-weight-normal: 400; + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: #030213; + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --font-weight-medium: 500; + --font-weight-normal: 400; + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-input-background: var(--input-background); + --color-switch-background: var(--switch-background); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + + /* body { + @apply bg-background text-foreground; + } */ +} + +/** + * Base typography. This is not applied to elements which have an ancestor with a Tailwind text class. + */ +@layer base { + :where(:not(:has([class*=' text-']), :not(:has([class^='text-'])))) { + h1 { + font-size: var(--text-2xl); + font-weight: var(--font-weight-medium); + line-height: 1.5; + } + + h2 { + font-size: var(--text-xl); + font-weight: var(--font-weight-medium); + line-height: 1.5; + } + + h3 { + font-size: var(--text-lg); + font-weight: var(--font-weight-medium); + line-height: 1.5; + } + + h4 { + font-size: var(--text-base); + font-weight: var(--font-weight-medium); + line-height: 1.5; + } + + p { + font-size: var(--text-base); + font-weight: var(--font-weight-normal); + line-height: 1.5; + } + + label { + font-size: var(--text-base); + font-weight: var(--font-weight-medium); + line-height: 1.5; + } + + button { + font-size: var(--text-base); + font-weight: var(--font-weight-medium); + line-height: 1.5; + } + + input { + font-size: var(--text-base); + font-weight: var(--font-weight-normal); + line-height: 1.5; + } + } +} + +html { + font-size: var(--font-size); +} From 3c3ec94cf05211f938a97e832aec5d91f15559df Mon Sep 17 00:00:00 2001 From: tali-creator Date: Thu, 4 Dec 2025 03:59:39 +0100 Subject: [PATCH 6/9] refactored --- app/dashboard/help/page.tsx | 562 +++++++++++++ app/dashboard/history/page.tsx | 361 +++++++++ app/dashboard/layout.tsx | 63 +- app/dashboard/merchant/page.tsx | 298 +++++++ app/dashboard/notifications/page.tsx | 303 +++++++ app/dashboard/page.tsx | 154 ++-- app/dashboard/profile/page.tsx | 83 ++ app/dashboard/receive/page.tsx | 201 +++++ app/dashboard/send/page.tsx | 419 ++++++++++ app/dashboard/service/airtime/page.tsx | 5 + app/dashboard/service/data/page.tsx | 5 + app/dashboard/service/electricity/page.tsx | 5 + app/dashboard/service/layout.tsx | 9 + app/dashboard/service/page.tsx | 10 + app/dashboard/split/page.tsx | 222 ++++++ app/dashboard/swap/page.tsx | 463 +++++++++++ app/dashboard/test/page.tsx | 10 + app/dashboard/topup/page.tsx | 143 ++++ app/globals.css | 2 + components/dashboard/mobile-bottom-nav.tsx | 47 +- .../purchase/PurchaseCommon/AmountGrid.tsx | 103 +++ .../purchase/PurchaseCommon/CustomerInput.tsx | 73 ++ .../PurchaseCommon/DataPlanSelect.tsx | 106 +++ .../PurchaseCommon/MeterTypeSelect.tsx | 50 ++ .../PurchaseCommon/ProviderSelect.tsx | 93 +++ .../PurchaseCommon/PurchasePinDialog.tsx | 84 ++ .../purchase/PurchaseCommon/SuccessScreen.tsx | 238 ++++++ .../dashboard/purchase/PurchaseFlow.tsx | 212 +++++ .../PurchaseSteps/PaymentSelection.tsx | 81 ++ .../PurchaseSteps/ProviderSelection.tsx | 201 +++++ .../PurchaseSteps/ReviewConfirmation.tsx | 135 ++++ components/dashboard/quick-actions.tsx | 68 +- components/dashboard/recent-activity.tsx | 23 +- components/dashboard/stats-cards.tsx | 8 +- components/dashboard/tabs/dashboard.tsx | 2 +- components/dashboard/tabs/electricity.tsx | 2 +- components/dashboard/top-nav.tsx | 92 +-- components/dashboard/wallet-overview.tsx | 84 +- components/hooks/useAddresses.tsx | 32 - components/hooks/useApiQuery.ts | 132 +++ components/hooks/useAuthQuery.ts | 17 + components/hooks/useBack.ts | 22 + components/hooks/useCachedQuery.ts | 88 -- components/hooks/useCommonOperations.ts | 42 + components/hooks/useExchangeRate.tsx | 10 +- components/hooks/useMerchantPayments.ts | 255 +----- components/hooks/useNotifications.ts | 6 +- components/hooks/usePin.tsx | 4 +- components/hooks/usePurchaseConfig.ts | 214 +++++ components/hooks/usePurchaseFlow.ts | 750 ++++++++++++++++++ components/hooks/useSilentQuery.ts | 164 ---- components/hooks/useTokenBalance.ts | 460 ++++++----- components/hooks/useTokenMonitor.ts | 1 - components/hooks/useTransactions.ts | 4 +- components/hooks/useWalletData.ts | 249 +----- components/modals/TransactionStatus.tsx | 514 ++++++++++++ components/modals/walletSAndBalance.tsx | 182 +++++ components/ui/AddressCopyButton.tsx | 68 ++ components/ui/AddressInput.tsx | 42 + components/ui/AmountInput.tsx | 36 + components/ui/CardContainer.tsx | 63 ++ components/ui/LoadingState.tsx | 31 + components/ui/StepsGuide.tsx | 31 + components/ui/notification.tsx | 14 +- lib/utils.ts | 68 +- lib/utils/qr-utils.ts | 376 +++++++++ lib/utils/storage-utils.ts | 82 ++ lib/utils/token-utils.ts | 274 +++++++ package-lock.json | 35 +- package.json | 2 + types/hooks.ts | 12 + 71 files changed, 8089 insertions(+), 1211 deletions(-) create mode 100644 app/dashboard/help/page.tsx create mode 100644 app/dashboard/history/page.tsx create mode 100644 app/dashboard/merchant/page.tsx create mode 100644 app/dashboard/notifications/page.tsx create mode 100644 app/dashboard/profile/page.tsx create mode 100644 app/dashboard/receive/page.tsx create mode 100644 app/dashboard/send/page.tsx create mode 100644 app/dashboard/service/airtime/page.tsx create mode 100644 app/dashboard/service/data/page.tsx create mode 100644 app/dashboard/service/electricity/page.tsx create mode 100644 app/dashboard/service/layout.tsx create mode 100644 app/dashboard/service/page.tsx create mode 100644 app/dashboard/split/page.tsx create mode 100644 app/dashboard/swap/page.tsx create mode 100644 app/dashboard/test/page.tsx create mode 100644 app/dashboard/topup/page.tsx create mode 100644 components/dashboard/purchase/PurchaseCommon/AmountGrid.tsx create mode 100644 components/dashboard/purchase/PurchaseCommon/CustomerInput.tsx create mode 100644 components/dashboard/purchase/PurchaseCommon/DataPlanSelect.tsx create mode 100644 components/dashboard/purchase/PurchaseCommon/MeterTypeSelect.tsx create mode 100644 components/dashboard/purchase/PurchaseCommon/ProviderSelect.tsx create mode 100644 components/dashboard/purchase/PurchaseCommon/PurchasePinDialog.tsx create mode 100644 components/dashboard/purchase/PurchaseCommon/SuccessScreen.tsx create mode 100644 components/dashboard/purchase/PurchaseFlow.tsx create mode 100644 components/dashboard/purchase/PurchaseSteps/PaymentSelection.tsx create mode 100644 components/dashboard/purchase/PurchaseSteps/ProviderSelection.tsx create mode 100644 components/dashboard/purchase/PurchaseSteps/ReviewConfirmation.tsx delete mode 100644 components/hooks/useAddresses.tsx create mode 100644 components/hooks/useApiQuery.ts create mode 100644 components/hooks/useAuthQuery.ts create mode 100644 components/hooks/useBack.ts delete mode 100644 components/hooks/useCachedQuery.ts create mode 100644 components/hooks/useCommonOperations.ts create mode 100644 components/hooks/usePurchaseConfig.ts create mode 100644 components/hooks/usePurchaseFlow.ts delete mode 100644 components/hooks/useSilentQuery.ts create mode 100644 components/modals/TransactionStatus.tsx create mode 100644 components/modals/walletSAndBalance.tsx create mode 100644 components/ui/AddressCopyButton.tsx create mode 100644 components/ui/AddressInput.tsx create mode 100644 components/ui/AmountInput.tsx create mode 100644 components/ui/CardContainer.tsx create mode 100644 components/ui/LoadingState.tsx create mode 100644 components/ui/StepsGuide.tsx create mode 100644 lib/utils/qr-utils.ts create mode 100644 lib/utils/storage-utils.ts create mode 100644 lib/utils/token-utils.ts create mode 100644 types/hooks.ts diff --git a/app/dashboard/help/page.tsx b/app/dashboard/help/page.tsx new file mode 100644 index 0000000..58d1d98 --- /dev/null +++ b/app/dashboard/help/page.tsx @@ -0,0 +1,562 @@ +"use client"; + +import { Card } from "@/components/ui/Card"; +import { + Search, + MessageCircle, + BookOpen, + HelpCircle, + Mail, + Globe, + FileText, +} from "lucide-react"; +import React, { useState, useEffect } from "react"; +import Button from "@/components/ui/Button"; + +interface FAQItem { + id: string; + question: string; + answer: string; + category: string; +} + +interface Article { + id: string; + title: string; + description: string; + category: string; + readTime: string; +} + +interface ContactOption { + id: string; + title: string; + description: string; + icon: React.ReactNode; + action: string; +} + +export default function Help() { + const [searchQuery, setSearchQuery] = useState(""); + const [activeCategory, setActiveCategory] = useState("all"); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage] = useState(5); + const [displayedPages, setDisplayedPages] = useState([]); + const [totalPages, setTotalPages] = useState(0); + + // FAQ data + const faqs: FAQItem[] = [ + { + id: "1", + question: "How do I create a payment split?", + answer: + "To create a payment split, go to the 'Split' tab, click the '+' Button, add recipients with their percentages, and confirm the split creation.", + category: "payments", + }, + { + id: "2", + question: "What tokens are supported for swapping?", + answer: + "We currently support USDT, USDC, STRK, and ETH for swapping to Naira.", + category: "swap", + }, + { + id: "3", + question: "How do I generate a QR code for payments?", + answer: + "Navigate to the 'QRPayment' tab, enter the amount in Naira, select a token, and click 'Create Payment Request' to generate your QR code.", + category: "payments", + }, + { + id: "4", + question: "How long do transactions take to process?", + answer: + "Most transactions are processed within 2-5 minutes, but Starknet transactions can sometimes take up to 15 minutes during high network congestion.", + category: "general", + }, + { + id: "5", + question: "How do I connect my wallet?", + answer: + "Click the 'Connect Wallet' Button in the header and select your preferred wallet provider (ArgentX or Braavos).", + category: "wallets", + }, + { + id: "6", + question: "What are the transaction fees?", + answer: + "We charge a 0.5% fee on all transactions. Network (gas) fees are additional and vary based on blockchain congestion.", + category: "fees", + }, + { + id: "7", + question: "How do I export my transaction history?", + answer: + "Go to the 'History' tab and click the 'Export CSV' Button to download your transaction history.", + category: "history", + }, + { + id: "8", + question: "Can I use testnet and mainnet?", + answer: + "Yes, your wallet addresses work on both testnet and mainnet. Switching networks only changes which blockchain you're viewing.", + category: "wallets", + }, + { + id: "9", + question: "How do I update my profile information?", + answer: + "Navigate to the 'Profile' tab to edit your personal information and linked bank accounts.", + category: "profile", + }, + { + id: "10", + question: "What should I do if a transaction fails?", + answer: + "Failed transactions typically refund automatically. If you don't receive a refund within 24 hours, please contact our support team.", + category: "troubleshooting", + }, + { + id: "11", + question: "How secure is my data?", + answer: + "We use industry-standard encryption and security practices. Your private keys never leave your device, and we don't have access to your funds.", + category: "security", + }, + { + id: "12", + question: "Can I use multiple bank accounts?", + answer: + "Yes, you can link multiple bank accounts in your profile settings and select which one to use for transactions.", + category: "banking", + }, + ]; + + // Help articles + const articles: Article[] = [ + { + id: "1", + title: "Getting Started Guide", + description: + "A comprehensive guide to setting up your account and making your first transaction.", + category: "guides", + readTime: "5 min read", + }, + { + id: "2", + title: "Understanding Transaction Fees", + description: + "Learn about our fee structure and how to minimize transaction costs.", + category: "fees", + readTime: "3 min read", + }, + { + id: "3", + title: "Troubleshooting Common Issues", + description: "Solutions for the most frequently encountered problems.", + category: "troubleshooting", + readTime: "7 min read", + }, + { + id: "4", + title: "Security Best Practices", + description: "How to keep your account and funds secure.", + category: "security", + readTime: "4 min read", + }, + { + id: "5", + title: "Advanced Split Configurations", + description: "How to set up complex payment distribution scenarios.", + category: "payments", + readTime: "6 min read", + }, + ]; + + // Contact options + const contactOptions: ContactOption[] = [ + { + id: "1", + title: "Live Chat", + description: "Get immediate assistance from our support team", + icon: , + action: "Start Chat", + }, + { + id: "2", + title: "Email Support", + description: + "Send us a detailed message and we'll respond within 24 hours", + icon: , + action: "Send Email", + }, + { + id: "3", + title: "Knowledge Base", + description: "Browse our comprehensive documentation and guides", + icon: , + action: "Browse Articles", + }, + ]; + + // Categories for filtering + const categories = [ + { id: "all", name: "All Topics" }, + { id: "payments", name: "Payments" }, + { id: "wallets", name: "Wallets" }, + { id: "swap", name: "Swap" }, + { id: "fees", name: "Fees" }, + { id: "security", name: "Security" }, + { id: "troubleshooting", name: "Troubleshooting" }, + ]; + + // Filter FAQs based on search query and active category + const filteredFaqs = faqs.filter( + (faq) => + (faq.question.toLowerCase().includes(searchQuery.toLowerCase()) || + faq.answer.toLowerCase().includes(searchQuery.toLowerCase())) && + (activeCategory === "all" || faq.category === activeCategory) + ); + + // Get current items for pagination + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredFaqs.slice(indexOfFirstItem, indexOfLastItem); + + useEffect(() => { + // Calculate total pages + const total = Math.ceil(filteredFaqs.length / itemsPerPage); + setTotalPages(total); + + // Update displayed pages + updateDisplayedPages(currentPage, total); + }, [currentPage, searchQuery, activeCategory, filteredFaqs.length]); + + const updateDisplayedPages = (page: number, total: number) => { + if (total <= 1) { + setDisplayedPages([]); + return; + } + + const pages: number[] = []; + const maxVisible = 5; + let startPage = Math.max(1, page - Math.floor(maxVisible / 2)); + let endPage = startPage + maxVisible - 1; + + if (endPage > total) { + endPage = total; + startPage = Math.max(1, endPage - maxVisible + 1); + } + + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + + setDisplayedPages(pages); + }; + + const handlePageChange = (pageNumber: number) => { + if (pageNumber < 1 || pageNumber > totalPages) return; + setCurrentPage(pageNumber); + }; + + const renderPageNumbers = () => { + if (totalPages <= 1) return null; + + return ( + <> + {displayedPages[0] > 1 && ( + <> + + {displayedPages[0] > 2 && ( + ... + )} + + )} + + {displayedPages.map((number) => ( + + ))} + + {displayedPages[displayedPages.length - 1] < totalPages && ( + <> + {displayedPages[displayedPages.length - 1] < totalPages - 1 && ( + ... + )} + + + )} + + ); + }; + + // Live chat would typically open a chat widget; here we just alert for demo + const handleContactAction = (actionType: string) => { + switch (actionType) { + case "Start Chat": + alert("Live chat would open here"); + break; + case "Send Email": + window.location.href = "mailto:support@yourplatform.com"; + break; + case "Browse Articles": + // Scroll to articles section + document + .getElementById("knowledge-base") + ?.scrollIntoView({ behavior: "smooth" }); + break; + default: + break; + } + }; + + return ( +
+
+

Help Center

+

+ Find answers to common questions and get support +

+ + {/* Search Section */} +
+
+ + { + setSearchQuery(e.target.value); + setCurrentPage(1); + }} + className="w-full pl-10 pr-4 py-3 bg-background border border-border rounded-md text-foreground placeholder:text-muted-foreground" + /> +
+
+ + {/* Category Filters */} +
+ {categories.map((category) => ( + + ))} +
+ + {/* Contact Options */} +
+ {contactOptions.map((option) => ( + +
+ {option.icon} +
+

+ {option.title} +

+

+ {option.description} +

+ +
+ ))} +
+ + {/* FAQ Section */} +
+

+ Frequently Asked Questions +

+ + {currentItems.length > 0 ? ( +
+ {currentItems.map((faq) => ( + +

+ + {faq.question} +

+

+ {faq.answer} +

+
+ ))} +
+ ) : ( + + +

No questions found

+

+ Try a different search term or category +

+
+ )} + + {/* Pagination for FAQs */} + {filteredFaqs.length > 0 && ( +
+
+ Showing {indexOfFirstItem + 1} to{" "} + {Math.min(indexOfLastItem, filteredFaqs.length)} of{" "} + {filteredFaqs.length} results +
+
+ + + {renderPageNumbers()} + + +
+
+ )} +
+ + {/* Knowledge Base Section (temporarily removed) +
+

+ Knowledge Base +

+
+ {articles.map((article) => ( + +
+ +

+ {article.title} +

+
+

+ {article.description} +

+
+ + {article.category} + + + {article.readTime} + +
+
+ ))} +
+
+ */} + + {/* Additional Help Section */} + +
+
+ {/*

+ Still need help? +

*/} +

+ Still need help? + Our support team is available 24/7 to assist you with any issues + or questions. +

+
+
+ + +
+
+
+
+
+ ); +} diff --git a/app/dashboard/history/page.tsx b/app/dashboard/history/page.tsx new file mode 100644 index 0000000..ab7f456 --- /dev/null +++ b/app/dashboard/history/page.tsx @@ -0,0 +1,361 @@ +"use client"; + +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/cards"; +import { Button } from "@/components/ui/buttons"; +import { Badge } from "@/components/ui/badge"; +import { + ArrowUpRight, + ArrowDownLeft, + ArrowUpDown, + Users, + DollarSign, + ChevronRight, +} from "lucide-react"; +import React, { useState, useEffect } from "react"; +import { useTransactions } from "@/components/hooks/useTransactions"; + +declare global { + interface Date { + toRelativeTime(): string; + } +} + +// Add toRelativeTime method to Date prototype +Date.prototype.toRelativeTime = function () { + const now = new Date("2025-09-25T08:40:00+01:00"); + const diffMs = now.getTime() - this.getTime(); + const diffMinutes = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMinutes < 60) return `${diffMinutes} min ago`; + if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`; + return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`; +}; + +interface Transaction { + id: string; + type: string; + chain: string; + amount: string; + currency: string; + fromAddress: string; + toAddress: string; + txHash: string; + status: string; + timestamp: string; +} + +const ActivityIcon = ({ type, status }: { type: string; status: string }) => { + const baseClasses = "p-2 hidden sm:flex rounded-full"; + + if (status === "pending") { + return ( +
+
+
+ ); + } + + if (status === "failed") { + return
⚠️
; + } + + switch (type) { + case "incoming": + return ( +
+ +
+ ); + case "outgoing": + return ( +
+ +
+ ); + case "swap": + return ( +
+ +
+ ); + case "split": + return ( +
+ +
+ ); + default: + return ( +
+ +
+ ); + } +}; + +export default function History() { + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage] = useState(10); + const [displayedPages, setDisplayedPages] = useState([]); + const [totalPages, setTotalPages] = useState(0); + const { transactions, pagination, error, refetch } = useTransactions({ + page: currentPage, + limit: itemsPerPage, + }); + + const updateDisplayedPages = (page: number, total: number) => { + if (total <= 1) { + setDisplayedPages([]); + return; + } + + const pages: number[] = []; + const maxVisible = 5; + let startPage = Math.max(1, page - Math.floor(maxVisible / 2)); + let endPage = startPage + maxVisible - 1; + + if (endPage > total) { + endPage = total; + startPage = Math.max(1, endPage - maxVisible + 1); + } + + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + + setDisplayedPages(pages); + }; + + useEffect(() => { + if (pagination) { + setTotalPages(pagination.totalPages || 0); + updateDisplayedPages(currentPage, pagination.totalPages || 0); + } + }, [pagination, currentPage]); + + const filteredTransactions = transactions.filter( + (tx) => + tx.fromAddress.toLowerCase().includes(searchQuery.toLowerCase()) || + tx.toAddress.toLowerCase().includes(searchQuery.toLowerCase()) || + tx.type.toLowerCase().includes(searchQuery.toLowerCase()) || + tx.status.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const handlePageChange = (pageNumber: number) => { + if (pageNumber < 1 || pageNumber > totalPages) return; + setCurrentPage(pageNumber); + }; + + const renderPageNumbers = () => { + if (totalPages <= 1) return null; + + return ( + <> + {displayedPages[0] > 1 && ( + <> + + {displayedPages[0] > 2 && ( + ... + )} + + )} + + {displayedPages.map((number) => ( + + ))} + + {displayedPages[displayedPages.length - 1] < totalPages && ( + <> + {displayedPages[displayedPages.length - 1] < totalPages - 1 && ( + ... + )} + + + )} + + ); + }; + + if (transactions.length === 0) { + // Render a nicer skeleton while initial transactions load (or no cached data) + return ( +
+
+ {[...Array(6)].map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
+
+ ); + } + + return ( +
+
+ + + + Transactions History + + + + + {error && ( +
+ Error: {error} + +
+ )} + + {transactions.length < 1 ? ( +
+
+ Loading... +
+
+ ) : filteredTransactions.length === 0 ? ( // UPDATE: Use filteredTransactions +
+ No transactions found +
+ ) : ( + filteredTransactions.map((tx) => ( +
+
+ +
+

+ {tx.type.charAt(0).toUpperCase() + tx.type.slice(1)}{" "} + {tx.type === "swap" + ? `(${tx.fromAddress} to ${tx.toAddress})` + : ""} +

+

+ {new Date(tx.timestamp).toRelativeTime()} +

+
+
+
+
+

+ {tx.type === "incoming" ? "+" : "-"} + {tx.amount} {tx.currency} +

+ + {tx.status} + +
+
+
+ )) + )} +
+
+ +
+
+ Showing {filteredTransactions.length} of {transactions.length}{" "} + results +
+
+ + + {renderPageNumbers()} + + +
+
+
+
+ ); +} diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx index 706f0a2..79cc6a3 100644 --- a/app/dashboard/layout.tsx +++ b/app/dashboard/layout.tsx @@ -1,9 +1,64 @@ +"use client"; + import type React from "react"; +import { useBack } from "@/components/hooks/useBack"; +import { ArrowLeft } from "lucide-react"; +import ProtectedRoute from "@/components/auth/protected-route"; +import { useNotifications } from "@/components/hooks/useNotifications"; +import { useTokenMonitor } from "@/components/hooks/useTokenMonitor"; +import { useRef, useEffect } from "react"; +import { useAuth } from "@/components/context/AuthContext"; +import { useDeposits } from "@/components/hooks"; +import { ToastContainer } from "@/components/modals/toastContainer"; +import { TokenExpiredDialog } from "@/components/modals/TokenExpiredDialog"; +import { MobileBottomNav } from "@/components/dashboard/mobile-bottom-nav"; +import { TopNav } from "@/components/dashboard/top-nav"; export default function RootLayout({ children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return
{children}
; +}: Readonly<{ children: React.ReactNode }>) { + const { checkDeposits } = useDeposits(); + const { token } = useAuth(); + + const { toasts, removeToast } = useNotifications(); + const checkRef = useRef(checkDeposits); + useEffect(() => { + checkRef.current = checkDeposits; + }, [checkDeposits]); + + useEffect(() => { + if (!token) return; + + checkRef.current(); + + const id = window.setInterval(() => { + if (token) checkRef.current(); + }, 20000); + + return () => window.clearInterval(id); + }, [token]); + + const { showExpiredDialog, handleRelogin } = useTokenMonitor(); + + return ( + +
+ + + +
+ + {children} +
+ + + +
+
+ ); } diff --git a/app/dashboard/merchant/page.tsx b/app/dashboard/merchant/page.tsx new file mode 100644 index 0000000..840f9c1 --- /dev/null +++ b/app/dashboard/merchant/page.tsx @@ -0,0 +1,298 @@ +"use client"; + +import React, { useCallback, useMemo, useState } from "react"; +import useExchangeRates from "@/components/hooks/useExchangeRate"; +import { QRCodeDisplay } from "@/components/modals/qr-code-display"; +import { useMerchantPayments } from "@/components/hooks/useMerchantPayments"; +import { AddressDropdown } from "@/components/modals/addressDropDown"; +import { useWalletData } from "@/components/hooks/useWalletData"; + +import { + generateCompatibleQRCode, + getCurrencySymbol, + supportsAmountInQR +} from "@/lib/utils/qr-utils"; +import { + getTokenRateKey, + getTokenChain, + formatBalance +} from "@/lib/utils/token-utils"; +import { CardContainer } from "@/components/ui/CardContainer"; +import { StepsGuide } from "@/components/ui/StepsGuide"; +import { ValidationError } from "@/components/modals/TransactionStatus"; +import { AmountInput } from "@/components/ui/AmountInput"; +import { LoadingState } from "@/components/ui/LoadingState"; +import { normalizeStarknetAddress } from "@/components/lib/utils"; + +export default function QrPayment() { + const [token, setToken] = useState("STARKNET"); + const [amount, setAmount] = useState(""); + const [description, setDescription] = useState(""); + const [showQR, setShowQR] = useState(false); + const [qrData, setQrData] = useState(""); + const [isProcessing, setIsProcessing] = useState(false); + const [paymentId, setPaymentId] = useState(""); + const [localError, setLocalError] = useState(null); + const [paymentStatus, setPaymentStatus] = useState(""); + + const { addresses } = useWalletData(); + const { rates, isLoading: ratesLoading } = useExchangeRates(); + const { createPayment, isLoading: merchantLoading } = useMerchantPayments(); + + // Get the current wallet address for the selected token + const currentReceiverAddress = useMemo((): string => { + if (!addresses || addresses.length === 0) return ""; + + const chain = getTokenChain(token); + const addr = addresses.find((a) => a.chain === chain); + if (!addr) return ""; + + return normalizeStarknetAddress(addr.address, chain); + }, [addresses, token]); + + // Calculate token amount based on NGN input + const calculateTokenAmount = useCallback((): string => { + const ngnAmount = parseFloat(amount) || 0; + const rateKey = getTokenRateKey(token); + const rate = rateKey ? rates[rateKey] : 1; + + if (!rate || rate === 0) { + console.log("Using fallback rate for calculation"); + return (ngnAmount / 1500).toFixed(6); + } + + const tokenAmount = ngnAmount / rate; + return tokenAmount.toFixed(6); + }, [amount, rates, token]); + + // Handle token selection + const handleTokenSelect = (tkn: string) => { + setToken(tkn.toUpperCase()); + }; + + // Create payment request + const handleCreatePaymentRequest = async () => { + if (!amount || !currentReceiverAddress) { + setLocalError( + "Please enter an amount and ensure wallet address is available" + ); + return; + } + + setIsProcessing(true); + setLocalError(null); + + try { + const tokenAmount = calculateTokenAmount(); + const chain = getTokenChain(token); + + // Use the new QR utility + const qrResult = await generateCompatibleQRCode( + chain, + currentReceiverAddress, + { + amount: tokenAmount, + width: 200, + margin: 2, + errorCorrectionLevel: "M" as const, + } + ); + + // Prepare request body + const requestBody = { + amount: parseFloat(tokenAmount), + chain, + network: "mainnet" as const, + description: description || "QR Payment request", + ...getAddressFieldForChain(chain, currentReceiverAddress) + }; + + const response = await createPayment(requestBody); + + if (response?.payment) { + setPaymentId(response.payment.id || ""); + setPaymentStatus(response.payment.status); + setQrData(qrResult.dataUrl); + setShowQR(true); + } else { + throw new Error("Invalid response from server - no payment data"); + } + } catch (error: any) { + console.error("Error creating payment request:", error); + setLocalError(error.message || "Failed to create payment request"); + } finally { + setIsProcessing(false); + } + }; + + // Helper to get the correct address field name for API + const getAddressFieldForChain = (chain: string, address: string) => { + const fieldMap: Record> = { + bitcoin: { btcAddress: address }, + ethereum: { ethAddress: address }, + solana: { solAddress: address }, + starknet: { strkAddress: address }, + usdt_erc20: { usdtErc20Address: address }, + usdt_trc20: { usdtTrc20Address: address }, + polkadot: { dotAddress: address }, + stellar: { xmlAddress: address }, + }; + + return fieldMap[chain] || { address }; + }; + + // Close QR modal + const handleCloseQR = () => { + setShowQR(false); + setQrData(""); + setPaymentId(""); + setLocalError(null); + setAmount(""); + }; + + // Steps for instructions + const steps = [ + { + title: "Enter Amount", + description: "Specify the amount in NGN you want to receive", + }, + { + title: "Generate QR", + description: "Create a unique payment request QR code", + }, + { + title: "Share QR", + description: "Send the QR code or address to the payer", + }, + { + title: "Receive Payment", + description: "Funds will be credited after confirmation", + }, + ]; + + // Check if form can be submitted + const canSubmit = !isProcessing && + amount && + !ratesLoading && + currentReceiverAddress; + + // Show loading state while addresses are loading + if (addresses.length === 0) { + return ; + } + + return ( +
+
+ {/* Main Card */} + +

+ Create Payment Request +

+ +
+
+ {/* Token Selection */} +
+ + +
+ + {/* Amount Input */} +
+ + +

+ ≈ {calculateTokenAmount()} {getCurrencySymbol(getTokenChain(token))} +

+
+ + {/* Description Input */} +
+ + setDescription(e.target.value)} + placeholder="Describe the payment purpose" + className="w-full p-3 rounded-lg bg-muted placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors" + disabled={merchantLoading || ratesLoading || isProcessing} + /> +
+ + +
+ + {/* Error Message */} + {localError && !showQR && ( + + )} + + {/* Generate Button */} + + + {/* Additional Info */} +
+

+ Note: QR code includes amount in {getCurrencySymbol(getTokenChain(token))} + {supportsAmountInQR(getTokenChain(token)) + ? " - compatible with most wallets" + : " - some wallets may not support amount display" + } +

+ {currentReceiverAddress && ( +

+ Receiving address: {currentReceiverAddress.slice(0, 12)}...{currentReceiverAddress.slice(-6)} +

+ )} +
+
+
+ + {/* Instructions Card */} + +
+ + {/* QR Code Modal */} + {showQR && ( + + )} +
+ ); +} \ No newline at end of file diff --git a/app/dashboard/notifications/page.tsx b/app/dashboard/notifications/page.tsx new file mode 100644 index 0000000..32fcd7a --- /dev/null +++ b/app/dashboard/notifications/page.tsx @@ -0,0 +1,303 @@ +"use client"; + +import { Bell, Check } from "lucide-react"; +import React, { useState, useEffect, useMemo } from "react"; // Remove SetStateAction import +import { Card } from "@/components/ui/Card"; +import { useNotifications } from "@/components/hooks/useNotifications"; // Fixed path +import ViewNotificationDetails from "@/components/modals/view-notification-details"; +import { FrontendNotification } from "@/types"; + +export default function Notifications() { + const { + notifications, + error, + markAsRead, + markAllAsRead, + } = useNotifications(); + const [activeTab, setActiveTab] = useState<"today" | "this-week" | "earlier">( + "today" + ); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage] = useState(10); + const [showNotifDetails, setShowNotifDetails] = useState(false); + const [content, setContent] = useState(); + + // Use useMemo for filtered notifications to prevent unnecessary recalculations + const filteredNotifications = useMemo(() => { + return notifications.filter((notif) => notif.category === activeTab); + }, [notifications, activeTab]); + + // Use useMemo for pagination data + const paginationData = useMemo(() => { + const totalPages = Math.ceil(filteredNotifications.length / itemsPerPage); + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredNotifications.slice( + indexOfFirstItem, + indexOfLastItem + ); + + return { totalPages, indexOfLastItem, indexOfFirstItem, currentItems }; + }, [filteredNotifications, currentPage, itemsPerPage]); + + const { totalPages, indexOfLastItem, indexOfFirstItem, currentItems } = + paginationData; + + // Calculate displayed pages + const displayedPages = useMemo(() => { + const total = totalPages; + if (total <= 1) return []; + + const pages: number[] = []; + const maxVisible = 5; + let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2)); + let endPage = startPage + maxVisible - 1; + + if (endPage > total) { + endPage = total; + startPage = Math.max(1, endPage - maxVisible + 1); + } + + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + + return pages; + }, [currentPage, totalPages]); + + const handleShowNotif = (id: string) => { + const selectedNotif = notifications.find((notif) => notif.id === id); + setContent(selectedNotif); + setShowNotifDetails(true); + }; + + const handleClearAll = () => { + setCurrentPage(1); + }; + + const handlePageChange = (pageNumber: number) => { + if (pageNumber < 1 || pageNumber > totalPages) return; + setCurrentPage(pageNumber); + }; + + const handleTabChange = (tab: "today" | "this-week" | "earlier") => { + setActiveTab(tab); + setCurrentPage(1); + }; + + const renderPageNumbers = () => { + if (totalPages <= 1) return null; + + return ( + <> + {displayedPages[0] > 1 && ( + <> + + {displayedPages[0] > 2 && ( + ... + )} + + )} + + {displayedPages.map((number) => ( + + ))} + + {displayedPages[displayedPages.length - 1] < totalPages && ( + <> + {displayedPages[displayedPages.length - 1] < totalPages - 1 && ( + ... + )} + + + )} + + ); + }; + + if (!notifications) { + return ( +
+
+
Loading notifications...
+
+
+ ); + } + + if (error) { + return ( +
+
+
{error}
+
+
+ ); + } + + return ( +
+
+

Notifications

+
+ + +
+
+ {/* Tabs */} +
+ {(["today", "this-week", "earlier"] as const).map((tab) => ( + + ))} +
+ {/* Notification List */} +
+ {currentItems.length > 0 ? ( + currentItems.map((notif) => ( + + )) + ) : ( + + +

No notifications

+

You're all caught up!

+
+ )} +
+ {/* Pagination */} + {filteredNotifications.length > 0 && ( +
+
+ Showing {indexOfFirstItem + 1} to{" "} + {Math.min(indexOfLastItem, filteredNotifications.length)} of{" "} + {filteredNotifications.length} results +
+
+ + + {renderPageNumbers()} + + +
+
+ )} + {content && ( + + )}{" "} +
+ ); +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index fea1442..6f84cf9 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,95 +1,95 @@ "use client"; -import { useEffect, useRef, useState } from "react"; -import DashboardHome from "@/components/dashboard/tabs/dashboard"; -import QrPayment from "@/components/dashboard/tabs/qr-payment"; -import PaymentSplit from "@/components/dashboard/tabs/payment-split"; -import Swap from "@/components/dashboard/tabs/swap"; -import Profile from "@/components/dashboard/tabs/profile"; -// import AuthPage from "@/components/auth/AuthPage"; -import Logout from "@/components/dashboard/tabs/logout"; -import CreateAddressTab from "@/components/dashboard/tabs/create-address"; -import History from "@/components/dashboard/tabs/history"; -import Notifications from "@/components/dashboard/tabs/notifications"; -import Help from "@/components/dashboard/tabs/help"; -import ProtectedRoute from "@/components/auth/protected-route"; -import { SideNav } from "@/components/dashboard/side-nav"; -import { TopNav } from "@/components/dashboard/top-nav"; -import { MobileBottomNav } from "@/components/dashboard/mobile-bottom-nav"; -import SendFunds from "@/components/dashboard/tabs/send-funds"; -import { ToastContainer } from "@/components/modals/toastContainer"; -import { useNotifications } from "@/components/hooks/useNotifications"; -import TopUp from "@/components/dashboard/tabs/top-up"; -import { useDeposits } from "@/components/hooks"; +import { Card } from "@/components/ui/Card"; +import Button from "@/components/ui/Button"; +import { StatsCards } from "@/components/dashboard/stats-cards"; +import { QuickActions } from "@/components/dashboard/quick-actions"; +import { RecentActivity } from "@/components/dashboard/recent-activity"; +import { WalletOverview } from "@/components/dashboard/wallet-overview"; import { useAuth } from "@/components/context/AuthContext"; -import { useTokenMonitor } from "@/components/hooks/useTokenMonitor"; -import { TokenExpiredDialog } from "@/components/modals/TokenExpiredDialog"; -import Services from "@/components/dashboard/tabs/services"; +import { useState } from "react"; -export default function Dashboard() { - const { checkDeposits } = useDeposits(); - const { token } = useAuth(); +interface RecentActivity { + id: string; + type: "incoming" | "outgoing" | "swap" | "split"; + amount: string; + token: string; + description: string; + timestamp: string; + status: "completed" | "pending" | "failed"; +} - const [activeTab, setActiveTab] = useState("Dashboard"); - const { toasts, removeToast } = useNotifications(); - const checkRef = useRef(checkDeposits); - useEffect(() => { - checkRef.current = checkDeposits; - }, [checkDeposits]); +export interface DashboardProps { + activeTab: React.Dispatch>; +} - useEffect(() => { - // Only start checking deposits once we have an auth token. - if (!token) return; +export default function DashboardHome({ activeTab }: DashboardProps) { + const { user } = useAuth(); - checkRef.current(); + const [hideBalalance, setHideBalance] = useState(false); - const id = window.setInterval(() => { - if (token) checkRef.current(); - }, 20000); + const handleViewBalance = () => { + setHideBalance(!hideBalalance); + }; - return () => window.clearInterval(id); - }, [token]); + return ( +
+ {/* Header */} +
+
+

+ Welcome back, {user?.firstName?.toLocaleUpperCase()} +

+

+ {"Ready to manage your finances? Let's make some magic happen."} +

+
- const { showExpiredDialog, handleRelogin } = useTokenMonitor(); - return ( - -
-
- +
-
- + {/* Stats Grid */} + -
- {activeTab === "Dashboard" && ( - - )} - + {/* Quick Actions */} +
+ {" "} +
- {activeTab === "QRPayment" && } - {activeTab === "Payment split" && } - {activeTab === "Swap" && } - {activeTab === "profile" && } - {activeTab === "Logout" && } - {activeTab === "Receive funds" && } - {activeTab === "History" && } - {activeTab === "Notification" && } - {activeTab === "Help" && } - {activeTab === "Send" && } - {activeTab === "Top Up" && } - {activeTab === "Services" && } - - {/* */} -
-
+ {/* Main Content Grid */} + + +
+
- -
-
+ {/* Bottom CTA */} + +
+
+

Need Help?

+

+ Check our Help or contact support +

+
+ +
+ + +
+
+
+
); } diff --git a/app/dashboard/profile/page.tsx b/app/dashboard/profile/page.tsx new file mode 100644 index 0000000..07bac41 --- /dev/null +++ b/app/dashboard/profile/page.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { RefreshCw } from "lucide-react"; +import { useAuth } from "@/components/context/AuthContext"; +import { Button } from "@/components/ui/buttons"; +import { ProfileForm } from "@/components/profile/profile-form"; +import { motion } from "framer-motion"; +import { ProfileAvatar } from "@/components/profile/profile-avatar"; +import { BankAccounts } from "@/components/profile/bank-accounts"; +import { IdentityVerification } from "@/components/profile/identity-verification"; +import { SetTransactionPin } from "@/components/profile/pin"; + +export default function ProfileSettingsPage() { + const { user} = useAuth(); + + console.log("XXXXXXXXX",user) + return ( +
+
+ + {/* Header */} +
+
+

Profile Settings

+

+ Manage your profile information and preferences +

+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/app/dashboard/receive/page.tsx b/app/dashboard/receive/page.tsx new file mode 100644 index 0000000..c6dbbfe --- /dev/null +++ b/app/dashboard/receive/page.tsx @@ -0,0 +1,201 @@ +"use client"; + +import React, { useState, useCallback, useEffect } from "react"; +import Image from "next/image"; +import { Copy, Check } from "lucide-react"; +import { AddressDropdown } from "@/components/modals/addressDropDown"; +import { useWalletData } from "@/components/hooks/useWalletData"; + +// Import the new utilities and components +import { + generateCompatibleQRCode, + getCurrencySymbol +} from "@/lib/utils/qr-utils"; +import { CardContainer } from "@/components/ui/CardContainer"; +import { LoadingState } from "@/components/ui/LoadingState"; +import { EmptyState } from "@/components/ui/LoadingState"; +import { AddressCopyButton } from "@/components/ui/AddressCopyButton"; +import { InstructionCard } from "@/components/ui/CardContainer"; +import { fixStarknetAddress, shortenAddress } from "@/components/lib/utils"; + +export default function ReceiveFunds() { + const [selectedToken, setSelectedToken] = useState("starknet"); + const [qrData, setQrData] = useState(""); + const [isLoading, setIsLoading] = useState(true); + + const { addresses } = useWalletData(); + + // Get selected token data + const selectedTokenData = addresses?.find( + (token) => token.chain === selectedToken + ); + + // Generate QR code when selected token changes + useEffect(() => { + const generateQrCode = async () => { + if (!selectedTokenData?.address) { + setQrData(""); + return; + } + + try { + let addressToUse = selectedTokenData.address; + + // Normalize Starknet address if needed + if (selectedTokenData.chain.toLowerCase() === "starknet") { + addressToUse = fixStarknetAddress( + addressToUse, + selectedTokenData.chain + ); + } + + const qrResult = await generateCompatibleQRCode( + selectedTokenData.chain, + addressToUse, + { + width: 200, + margin: 2, + errorCorrectionLevel: "M" as const, + } + ); + + setQrData(qrResult.dataUrl); + } catch (error) { + console.error("Error generating QR code:", error); + setQrData(""); + } + }; + + generateQrCode(); + }, [selectedTokenData]); + + // Check if wallet addresses are available + useEffect(() => { + if (addresses) { + setIsLoading(false); + } + }, [addresses]); + + // Handle token selection + const handleTokenSelect = useCallback((symbol: string) => { + setSelectedToken(symbol); + }, []); + + // Prepare shortened address for display + const getDisplayAddress = () => { + if (!selectedTokenData?.address) return ""; + + const normalizedAddress = fixStarknetAddress( + selectedTokenData.address, + selectedTokenData.chain + ); + + return shortenAddress(normalizedAddress, 10); + }; + + // Loading state + if (isLoading || addresses.length < 1) { + return ( + + ); + } + + // Empty state + if (!addresses || addresses.length === 0) { + return ( + + ); + } + + const instructions = [ + "Select the currency you want to receive", + "Share your QR code or wallet address", + "Wait for the sender to complete the transaction", + "Funds will appear in your wallet after confirmation", + ]; + + return ( +
+
+ {/* Main Card */} + + {/* Header */} +
+

Receive Funds

+

+ Select a currency and share your address to receive payments +

+
+ + {/* Token Selector */} +
+ +
+ + {/* QR Code Display */} +
+ {qrData ? ( +
+ QR Code +
+ ) : ( +
+ Loading QR... +
+ )} + +
+

+ {getCurrencySymbol(selectedTokenData?.chain || selectedToken)} Address +

+
+

+ {getDisplayAddress()} +

+ + {/* Using AddressCopyButton component */} + +
+ + {/* Network info */} + {selectedTokenData?.network && ( +

+ Network: {selectedTokenData.network} +

+ )} +
+
+
+ + {/* Instructions Card */} + +
+
+ ); +} \ No newline at end of file diff --git a/app/dashboard/send/page.tsx b/app/dashboard/send/page.tsx new file mode 100644 index 0000000..253dd42 --- /dev/null +++ b/app/dashboard/send/page.tsx @@ -0,0 +1,419 @@ +"use client" + +import { useAuth } from "@/components/context/AuthContext"; +import { useTokenBalance } from "@/components/hooks"; +import { + normalizeStarknetAddress, + shortenAddress, +} from "@/components/lib/utils"; +import { AddressDropdown } from "@/components/modals/addressDropDown"; +import { + StatusBadge, + TransactionStatus, +} from "@/components/modals/TransactionStatus"; +import { AddressCopyButton } from "@/components/ui/AddressCopyButton"; +import { AmountInput } from "@/components/ui/AmountInput"; +import { CardContainer, InstructionCard } from "@/components/ui/CardContainer"; +import { TransactionPinDialog } from "@/components/ui/transaction-pin-dialog"; +import { formatNGN } from "@/lib/utils/token-utils"; +import { AlertCircle, Loader2, ArrowUpRight } from "lucide-react"; +import { useState, useMemo, useCallback } from "react"; + +export default function SendFunds() { + const [selectedToken, setSelectedToken] = useState("ethereum"); + const [toAddress, setToAddress] = useState(""); + const [amount, setAmount] = useState(""); + const [isSending, setIsSending] = useState(false); + const [showPinDialog, setShowPinDialog] = useState(false); + const [txStatus, setTxStatus] = useState<{ + type: "success" | "error" | null; + message: string; + txHash?: string; + }>({ type: null, message: "" }); + + const { sendTransaction } = useAuth(); + + // Single hook for all token data + const { + walletTokens, + getTokenInfo, + getWalletBalance, + getWalletAddress, + getWalletNetwork, + hasWalletForToken, + getTokenSymbol, + getTokenName, + getTokenRate, + isLoading, + error, + formatBalance, + formatValue, + } = useTokenBalance(); + + const selectedTokenData = getTokenInfo(selectedToken); + + // Simplified derived values + const currentWalletBalance = getWalletBalance(selectedToken); + console.log("balance" , currentWalletBalance) + const currentWalletAddress = getWalletAddress(selectedToken); + const currentNetwork = getWalletNetwork(selectedToken); + const hasWalletForSelectedToken = hasWalletForToken(selectedToken); + + // Simplified NGN calculation + const ngnEquivalent = useMemo(() => { + if (!amount || isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) + return 0; + + const tokenSymbol = getTokenSymbol(selectedToken); + const tokenRate = getTokenRate(tokenSymbol); + return parseFloat(amount) * tokenRate; + }, [amount, selectedToken, getTokenRate, getTokenSymbol]); + + // Simplified validation + const validationError = useMemo(() => { + if (!hasWalletForSelectedToken) { + return "No wallet found for this currency"; + } + if (!toAddress.trim()) { + return "Recipient address is required"; + } + if (!amount || parseFloat(amount) <= 0) { + return "Amount must be greater than 0"; + } + if (parseFloat(amount) > currentWalletBalance) { + return "Insufficient balance"; + } + return null; + }, [hasWalletForSelectedToken, toAddress, amount, currentWalletBalance]); + + // Reset form + const resetForm = useCallback(() => { + setToAddress(""); + setAmount(""); + setTxStatus({ type: null, message: "" }); + }, []); + + // Handle token selection + const handleTokenSelect = useCallback( + (chain: string) => { + setSelectedToken(chain); + resetForm(); + }, + [resetForm] + ); + + // Handle send transaction with PIN + const handleSendWithPin = async (pin: string) => { + if (validationError) { + setTxStatus({ + type: "error", + message: validationError, + }); + setShowPinDialog(false); + return; + } + + setIsSending(true); + setTxStatus({ type: null, message: "" }); + + try { + let normalizedToAddress = toAddress.trim(); + let normalizedFromAddress = currentWalletAddress.trim(); + + // Special handling for Starknet addresses + if (selectedToken === "starknet") { + try { + normalizedToAddress = normalizeStarknetAddress( + normalizedToAddress, + selectedToken + ); + normalizedFromAddress = normalizeStarknetAddress( + normalizedFromAddress, + selectedToken + ); + console.log("Normalized Starknet address:", normalizedToAddress); + } catch (error) { + throw new Error( + error instanceof Error + ? `Invalid Starknet address: ${error.message}` + : "Invalid Starknet address format" + ); + } + } + + // Send transaction + const response = await sendTransaction({ + chain: selectedToken, + network: currentNetwork, + toAddress: normalizedToAddress, + amount: amount, + fromAddress: normalizedFromAddress, + transactionPin: pin, + }); + + setTxStatus({ + type: "success", + message: "Transaction sent successfully!", + txHash: response.txHash, + }); + + // Close PIN dialog + setShowPinDialog(false); + + // Reset form after 10 seconds + setTimeout(() => { + resetForm(); + }, 10000); + } catch (error: any) { + console.error("Transaction error:", error); + + let errorMessage = "Failed to send transaction. Please try again."; + + if (error.message) { + errorMessage = error.message; + } else if (typeof error === "string") { + errorMessage = error; + } + + setTxStatus({ + type: "error", + message: errorMessage, + }); + + // Close PIN dialog on error + setShowPinDialog(false); + } finally { + setIsSending(false); + } + }; + + // Handle send transaction (show PIN dialog) + const handleSendTransaction = () => { + if (validationError) { + setTxStatus({ + type: "error", + message: validationError, + }); + return; + } + + // Show PIN dialog instead of immediately sending + setShowPinDialog(true); + }; + + // Handle PIN dialog close + const handlePinDialogClose = () => { + setShowPinDialog(false); + }; + + // Loading state + + // Instructions for important notes + const importantNotes = [ + "Recipient does NOT need to be a Velo user", + "Only send to valid addresses for the selected currency", + "Transactions are irreversible once confirmed", + "Double-check addresses before sending", + ]; + + // Add Starknet-specific notes if needed + if (selectedToken === "starknet") { + importantNotes.push( + "Starknet wallets may need deployment (auto-handled)", + "Addresses will be auto-formatted with 0x prefix and padding" + ); + } + + return ( +
+
+ {/* Main Card */} + + {/* Header */} +
+

Send Payment

+

+ Transfer funds from your Velo wallet to any valid address +

+
+ +
+ {/* Transaction Status */} + {txStatus.type && ( + setTxStatus({ type: null, message: "" })} + showExplorerLink={!!txStatus.txHash} + autoDismiss={txStatus.type === "success"} + /> + )} + + {/* Wallet Status Warning */} + {!hasWalletForSelectedToken && ( +
+
+ + No Wallet Found +
+

+ No Velo wallet found for {getTokenName(selectedToken)}. You + can only send from wallets created in Velo. +

+
+ )} + +
+
+ + + {currentWalletAddress && ( +
+
+ + From Address: + + +
+

+ {shortenAddress(currentWalletAddress, 8)} +

+
+

+ Network: {currentNetwork} +

+ +
+
+ )} +
+ + {/* Right Column - Recipient Info */} +
+
+ + setToAddress(e.target.value)} + placeholder={`Enter ${ + selectedTokenData?.name || selectedToken + } address`} + className="w-full p-3 rounded-lg bg-muted placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors font-mono text-sm" + disabled={!hasWalletForSelectedToken || isSending} + /> + {selectedToken === "starknet" && toAddress && ( +

+ Tip: Address will be automatically formatted with 0x + prefix and proper padding +

+ )} +
+ + {/* Amount Input */} +
+ + +
+ + Available:{" "} + {currentWalletBalance}{" "} + {selectedTokenData?.symbol} + + {ngnEquivalent > 0 && ( + ≈ {formatNGN(ngnEquivalent)} + )} +
+
+
+
+ + {/* Send Button */} +
+ +
+ + {/* Network Info */} +
+

+ Sending on{" "} + {currentNetwork}{" "} + network +

+
+
+
+ + {/* Instructions Card */} + +
+ + {/* PIN Dialog */} + +
+ ); +} diff --git a/app/dashboard/service/airtime/page.tsx b/app/dashboard/service/airtime/page.tsx new file mode 100644 index 0000000..a20b29d --- /dev/null +++ b/app/dashboard/service/airtime/page.tsx @@ -0,0 +1,5 @@ +import PurchaseFlow from "@/components/dashboard/purchase/PurchaseFlow"; + +export default function AirtimePage() { + return ; +} \ No newline at end of file diff --git a/app/dashboard/service/data/page.tsx b/app/dashboard/service/data/page.tsx new file mode 100644 index 0000000..4915034 --- /dev/null +++ b/app/dashboard/service/data/page.tsx @@ -0,0 +1,5 @@ +import PurchaseFlow from "@/components/dashboard/purchase/PurchaseFlow"; + +export default function AirtimePage() { + return ; +} \ No newline at end of file diff --git a/app/dashboard/service/electricity/page.tsx b/app/dashboard/service/electricity/page.tsx new file mode 100644 index 0000000..777f63a --- /dev/null +++ b/app/dashboard/service/electricity/page.tsx @@ -0,0 +1,5 @@ +import PurchaseFlow from "@/components/dashboard/purchase/PurchaseFlow"; + +export default function AirtimePage() { + return ; +} diff --git a/app/dashboard/service/layout.tsx b/app/dashboard/service/layout.tsx new file mode 100644 index 0000000..a5fb4ba --- /dev/null +++ b/app/dashboard/service/layout.tsx @@ -0,0 +1,9 @@ +import React, { ReactNode } from 'react' + +export default function ServiceLayout({children}: Readonly<{children: ReactNode}>) { + return ( +
+ {children} +
+ ) +} diff --git a/app/dashboard/service/page.tsx b/app/dashboard/service/page.tsx new file mode 100644 index 0000000..296b8ea --- /dev/null +++ b/app/dashboard/service/page.tsx @@ -0,0 +1,10 @@ +"use client" + +import { useRouter } from 'next/navigation' + +export default function ServiceRoute() { + const router = useRouter() + return ( + router.push("/dashboard/service/airtime") + ) +} diff --git a/app/dashboard/split/page.tsx b/app/dashboard/split/page.tsx new file mode 100644 index 0000000..364d7e1 --- /dev/null +++ b/app/dashboard/split/page.tsx @@ -0,0 +1,222 @@ +"use client"; + +import AddSplit from "@/components/modals/add-split"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/cards"; +import { Button } from "@/components/ui/buttons"; +import { Loader2, Plus, Users } from "lucide-react"; +import React, { useCallback, useState, useEffect } from "react"; +import { useSplitPayments } from "@/components/hooks/useSplitPayments"; +import { TransactionPinDialog } from "@/components/ui/transaction-pin-dialog"; + +export default function PaymentSplit() { + const { + templates, + isLoading, + refetch, + executeSplitPayment, + toggleSplitPayment, + } = useSplitPayments(); + + const [addSplitModal, setAddSplitModal] = useState(false); + const [showPinDialog, setShowPinDialog] = useState(false); // Add PIN dialog state + const [selectedTemplateId, setSelectedTemplateId] = useState(null); // Track which template to execute + const [isExecuting, setIsExecuting] = useState(false); // Track execution loading state + + const refetchTemplates = useCallback(async () => { + await refetch(); + }, [refetch]); + + // Handle PIN confirmation for split payment execution + const handleExecuteWithPin = async (pin: string) => { + if (!selectedTemplateId) return; + + setIsExecuting(true); + try { + await executeSplitPayment(selectedTemplateId, pin); // Pass PIN to execute function + // Hook should automatically refresh templates after execution + } catch (error) { + console.error("Failed to execute:", error); + } finally { + setIsExecuting(false); + setShowPinDialog(false); + setSelectedTemplateId(null); + } + }; + + // Modified handleExecute to show PIN dialog + const handleExecute = async (id: string) => { + setSelectedTemplateId(id); + setShowPinDialog(true); + }; + + const handleToggle = async (id: string) => { + try { + await toggleSplitPayment(id); + // Hook automatically refreshes templates after toggle + } catch (error) { + console.error("Failed to toggle:", error); + } + }; + + const handleShowSplitModal = () => { + setAddSplitModal(true); + }; + + // Handle PIN dialog close + const handlePinDialogClose = () => { + setShowPinDialog(false); + setSelectedTemplateId(null); + }; + + return ( +
+
+

Split Payments

+ +
+ + {isLoading ? ( +
+ +
+ ) : templates.length === 0 ? ( + + +

+ No split created yet. Create your first payment split to get + started. +

+
+ ) : ( +
+ {templates.map((template) => ( + + + {template.title} + + +

{template.description}

+
+
+ Chain: + {template.chain.toUpperCase()} +
+
+ Total Amount: + {template.totalAmount} +
+
+ Recipients: + {template.recipientCount} +
+
+ Executions: + {template.executionCount} +
+
+ Status: + {template.status} +
+
+
+ {template.canExecute && ( + + )} + +
+
+
+ ))} +
+ )} + +
+ +

+ How It Works +

+
+
+
+ 1 +
+
+

Create Split

+

+ Add multiple recipients with their wallet addresses and + amounts +

+
+
+
+
+ 2 +
+
+

Save Template

+

+ Create reusable template on the backend +

+
+
+
+
+ 3 +
+
+

+ Execute Payments +

+

+ Distribute funds to all recipients in one go (PIN required) +

+
+
+
+
+
+ + {addSplitModal && ( + + )} + + {/* PIN Dialog for Split Payment Execution */} + +
+ ); +} \ No newline at end of file diff --git a/app/dashboard/swap/page.tsx b/app/dashboard/swap/page.tsx new file mode 100644 index 0000000..c89dd34 --- /dev/null +++ b/app/dashboard/swap/page.tsx @@ -0,0 +1,463 @@ +"use client"; + +import { Card } from "@/components/ui/Card"; +import { ChevronDown, Shuffle } from "lucide-react"; +import { useCallback, useState } from "react"; +import useExchangeRates from "@/components/hooks/useExchangeRate"; +import Image from "next/image"; + +export default function Swap() { + const [fromToken, setFromToken] = useState("USDT"); + const [toToken, setToToken] = useState("NGN"); + const [fromAmount, setFromAmount] = useState(""); + const [toAmount, setToAmount] = useState(""); + const [showFromDropdown, setShowFromDropdown] = useState(false); + const [showToDropdown, setShowToDropdown] = useState(false); + const [showConfirmModal, setShowConfirmModal] = useState(false); + const [showSuccessModal, setShowSuccessModal] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + + const { rates } = useExchangeRates(); + + const tokens = [ + { symbol: "USDT", name: "Tether", icon: "/usdtlogo.svg" }, + { symbol: "USDC", name: "USD Coin", icon: "🔵" }, + { symbol: "STRK", name: "Starknet", icon: "⭐" }, + { symbol: "ETH", name: "Ethereum", icon: "💎" }, + ]; + + const calculateExchange = useCallback( + (amount: string, from: string, to: string) => { + if (!amount || !rates[from as keyof typeof rates] || !rates[to as keyof typeof rates]) + return ""; + + const fromRate = rates[from as keyof typeof rates] || 0; + const toRate = rates[to as keyof typeof rates] || 1; + + if (from === "NGN") { + return (parseFloat(amount) / fromRate).toFixed(6); + } else if (to === "NGN") { + return (parseFloat(amount) * fromRate).toFixed(2); + } else { + return ((parseFloat(amount) * fromRate) / toRate).toFixed(6); + } + }, + [rates] + ); + + const handleFromAmountChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + if (/^\d*\.?\d*$/.test(value)) { + setFromAmount(value); + setToAmount(calculateExchange(value, fromToken, toToken)); + } + }, + [fromToken, toToken, calculateExchange] + ); + + const handleToAmountChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + if (/^\d*\.?\d*$/.test(value)) { + setToAmount(value); + setFromAmount(calculateExchange(value, toToken, fromToken)); + } + }, + [fromToken, toToken, calculateExchange] + ); + + const handleSwapTokens = useCallback(() => { + const tempFromToken = fromToken; + const tempFromAmount = fromAmount; + + setFromToken(toToken); + setToToken(tempFromToken); + setFromAmount(toAmount); + setToAmount(tempFromAmount); + }, [fromToken, toToken, fromAmount, toAmount]); + + const handleSwap = useCallback(() => { + if (!fromAmount) return; + setShowConfirmModal(true); + }, [fromAmount]); + + const confirmSwap = useCallback(async () => { + setShowConfirmModal(false); + setIsProcessing(true); + + setTimeout(() => { + setIsProcessing(false); + setShowSuccessModal(true); + setFromAmount(""); + setToAmount(""); + }, 3000); + }, []); + + const fees = fromAmount ? ( + + {(parseFloat(fromAmount) * 0.005).toFixed(6)} + + ) : ( + 0.0 + ); + + const receiveAmount = toAmount ? ( + + {( + parseFloat(toAmount) - + parseFloat( + fromAmount ? (parseFloat(fromAmount) * 0.005).toString() : "0" + ) + ).toFixed(2)} + + ) : ( + -- + ); + + return ( +
+ +
+
+

Swap

+

+ Exchange between different tokens and Naira +

+
+ +
+
+ +
+
+ + From + +
+
+
+
+ + + {showFromDropdown && ( +
+ + {tokens + .filter((token) => token.symbol !== toToken) + .map((token) => ( + + ))} +
+ )} +
+
+ + +
+
+
+ +
+ +
+ + +
+ + To + +
+
+
+ + + {showToDropdown && ( +
+ + {tokens + .filter((token) => token.symbol !== fromToken) + .map((token) => ( + + ))} +
+ )} +
+
+ + +
+
+
+
+ +
+
+ + Fees (0.5%) + + {fees} {fromToken} +
+
+ + You will receive + + + ~ {receiveAmount} {toToken === "NGN" ? "" : toToken} + +
+
+ +
+ +
+
+
+
+ + {showConfirmModal && ( +
+ +
+
+
+
+ +
+

+ Confirm Swap +

+

+ Swap {fromAmount} {fromToken} for {receiveAmount} {toToken}? +

+
+ +
+ + +
+
+
+
+ )} + + {showSuccessModal && ( +
+ +
+
+ + + +
+ +
+

+ Swap Successful! +

+

+ You received {receiveAmount} {toToken} +

+
+ + +
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/app/dashboard/test/page.tsx b/app/dashboard/test/page.tsx new file mode 100644 index 0000000..76776a5 --- /dev/null +++ b/app/dashboard/test/page.tsx @@ -0,0 +1,10 @@ +import WalletSAndBalance from '@/components/modals/walletSAndBalance' +import React from 'react' + +export default function TEst() { + return ( +
+ +
+ ) +} diff --git a/app/dashboard/topup/page.tsx b/app/dashboard/topup/page.tsx new file mode 100644 index 0000000..0d9eace --- /dev/null +++ b/app/dashboard/topup/page.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { useState } from "react"; +// Assuming Card component is styled like the one in the screenshot (dark, slightly translucent) +import { Card } from "@/components/ui/Card"; +import { ChevronLeft } from "lucide-react"; +// Components for each step (assuming they handle their own internal styling) +import TokenSelection from "@/components/modals/token-selection"; +import AmountEntry from "@/components/modals/amount-entry"; +import Confirmation from "@/components/modals/confirmation"; + +type TopUpStep = "selection" | "amount" | "confirmation"; + +const steps: TopUpStep[] = ["selection", "amount", "confirmation"]; +const stepTitles = { + selection: "Select Cryptocurrency", + amount: "Enter Amount", + confirmation: "Confirm Purchase", +}; + +export default function TopUp() { + const [currentStep, setCurrentStep] = useState("selection"); + const [selectedToken, setSelectedToken] = useState(""); + const [amountData, setAmountData] = useState({ + ngnAmount: "", + cryptoAmount: "", + }); + + const handleTokenSelect = (token: string) => { + setSelectedToken(token); + setCurrentStep("amount"); + }; + + const handleAmountSubmit = (ngnAmount: string, cryptoAmount: string) => { + setAmountData({ ngnAmount, cryptoAmount }); + setCurrentStep("confirmation"); + }; + + const handleBack = () => { + if (currentStep === "amount") { + setCurrentStep("selection"); + } else if (currentStep === "confirmation") { + setCurrentStep("amount"); + } + }; + + const handleComplete = () => { + // Reset flow after completion + setCurrentStep("selection"); + setSelectedToken(""); + setAmountData({ ngnAmount: "", cryptoAmount: "" }); + // In a real app, you might want to show a success message here before redirecting + }; + + const currentStepIndex = steps.indexOf(currentStep); + + return ( +
+
+ {/* Adjusted Card: slightly wider for better flow, using a richer dark background */} + + {/* Header */} +
+ {currentStep !== "selection" && ( + + )} +
+ {/* Stronger, more prominent title */} +

+ Buy Crypto +

+ {/* Current Step Title/Subtitle */} +

+ {stepTitles[currentStep]} +

+
+
+ + {/* Progress Steps: Modern Pill/Bar Indicator */} +
+ {steps.map((step, index) => ( +
+ {/* Step Marker Pill */} +
+ {/* Step Label (Hidden on small screens, optional) */} + + {stepTitles[step]} + +
+ ))} +
+ + {/* Step Content */} + {/* The min-h is kept to prevent layout shift */} +
+ {currentStep === "selection" && ( + + )} + {currentStep === "amount" && ( + + )} + {currentStep === "confirmation" && ( + + )} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index f9f2812..03ef8cd 100644 --- a/app/globals.css +++ b/app/globals.css @@ -111,6 +111,8 @@ /* Ethereum purple */ } + + .dark { /* Dark mode colors - Professional dark theme with VELO brand colors */ --background: #0f172a; diff --git a/components/dashboard/mobile-bottom-nav.tsx b/components/dashboard/mobile-bottom-nav.tsx index 716442a..2a5657e 100644 --- a/components/dashboard/mobile-bottom-nav.tsx +++ b/components/dashboard/mobile-bottom-nav.tsx @@ -1,49 +1,32 @@ -"use client" +"use client"; -import { Button } from "@/components/ui/buttons" -import { QrCode, Send, ArrowDownToLine, History, Home } from "lucide-react" - - -interface MobileBottomNavProps { - activeTab: string - setActiveTab: React.Dispatch> -} +import { QrCode, Send, ArrowDownToLine, History, Home, Bell, User, HistoryIcon } from "lucide-react"; +import Link from "next/link"; const navItems = [ - { icon: Home, label: "Dashboard", active: true }, - { icon: QrCode, label: "QRPayment", active: false }, - { icon: Send, label: "Send", active: false }, - { icon: ArrowDownToLine, label: "Receive funds", active: false }, - { icon: History, label: "History", active: false }, -] - -export function MobileBottomNav({ activeTab, setActiveTab }: MobileBottomNavProps) { - + { icon: Home, label: "Dashboard", link: "/dashboard" }, + { icon: Bell, label: "Notification", link: "/dashboard/notification" }, + { icon: HistoryIcon, label: "History", link: "/dashboard/profile" }, + { icon: User, label: "Profile", link: "/dashboard/profile" }, +]; +export function MobileBottomNav() { return (
{navItems.map((item, index) => ( - + ))}
- ) + ); } diff --git a/components/dashboard/purchase/PurchaseCommon/AmountGrid.tsx b/components/dashboard/purchase/PurchaseCommon/AmountGrid.tsx new file mode 100644 index 0000000..97f8dde --- /dev/null +++ b/components/dashboard/purchase/PurchaseCommon/AmountGrid.tsx @@ -0,0 +1,103 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { AlertCircle } from "lucide-react"; + +interface AmountGridProps { + value: string; + onChange: (amount: string) => void; + presetAmounts: number[]; + minAmount?: number; + maxAmount?: number; + className?: string; +} + +export function AmountGrid({ + value, + onChange, + presetAmounts, + minAmount, + maxAmount, + className = "", +}: AmountGridProps) { + const numericValue = parseFloat(value) || 0; + + const isBelowMin = + minAmount !== undefined && numericValue > 0 && numericValue < minAmount; + const isAboveMax = maxAmount !== undefined && numericValue > maxAmount; + + const filteredPresets = presetAmounts.filter((amount) => { + if (minAmount !== undefined && amount < minAmount) return false; + if (maxAmount !== undefined && amount > maxAmount) return false; + return true; + }); + + const handleCustomAmountChange = (e: React.ChangeEvent) => { + const val = e.target.value; + if (val === "" || /^\d*$/.test(val)) { + onChange(val); + } + }; + + return ( +
+
+ + {(minAmount || maxAmount) && ( + + {minAmount && `Min: ₦${minAmount.toLocaleString()}`} + {minAmount && maxAmount && " • "} + {maxAmount && `Max: ₦${maxAmount.toLocaleString()}`} + + )} +
+ + {filteredPresets.length > 0 && ( +
+ {filteredPresets.map((amount) => ( + onChange(amount.toString())} + className={`p-3 rounded-xl transition-all border ${ + value === amount.toString() + ? " bg-primary/10 text-primary font-semibold" + : " hover:border-primary/50 hover:bg-accent" + }`} + > + ₦{amount.toLocaleString()} + + ))} +
+ )} + +
+ + {(isBelowMin || isAboveMax) && ( +
+ +
+ )} +
+ + {(isBelowMin || isAboveMax) && ( +

+ {isBelowMin + ? `Amount must be at least ₦${minAmount?.toLocaleString()}` + : `Amount cannot exceed ₦${maxAmount?.toLocaleString()}`} +

+ )} +
+ ); +} \ No newline at end of file diff --git a/components/dashboard/purchase/PurchaseCommon/CustomerInput.tsx b/components/dashboard/purchase/PurchaseCommon/CustomerInput.tsx new file mode 100644 index 0000000..f02bf49 --- /dev/null +++ b/components/dashboard/purchase/PurchaseCommon/CustomerInput.tsx @@ -0,0 +1,73 @@ +import React from "react"; + +interface CustomerInputProps { + type: "airtime" | "data" | "electricity"; + value: string; + onChange: (value: string) => void; + config: { + customerLabel: string; + placeholder: string; + }; + className?: string; +} + +export function CustomerInput({ + type, + value, + onChange, + config, + className = "", +}: CustomerInputProps) { + const handleChange = (e: React.ChangeEvent) => { + let inputValue = e.target.value; + + // Phone number validation (numbers only) + if (type !== "electricity") { + inputValue = inputValue.replace(/[^\d]/g, ''); + // Limit to 10 digits for Nigerian phone numbers + if (inputValue.length > 10) { + inputValue = inputValue.substring(0, 10); + } + } + + onChange(inputValue); + }; + + const getInputType = () => { + if (type === "electricity") return "text"; + return "tel"; + }; + + return ( +
+ + +
+ {type !== "electricity" && ( +
+ +234 +
+ )} + + +
+ + {type !== "electricity" && value && value.length < 10 && ( +

+ Enter a 10-digit Nigerian phone number +

+ )} +
+ ); +} \ No newline at end of file diff --git a/components/dashboard/purchase/PurchaseCommon/DataPlanSelect.tsx b/components/dashboard/purchase/PurchaseCommon/DataPlanSelect.tsx new file mode 100644 index 0000000..e9360c2 --- /dev/null +++ b/components/dashboard/purchase/PurchaseCommon/DataPlanSelect.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import { motion } from "framer-motion"; + +interface DataPlan { + dataplanId: string; + name: string; + amount: string; + validity: string; + description?: string; +} + +interface DataPlanSelectProps { + dataPlans: DataPlan[]; + value: DataPlan | null; + onSelect: (plan: DataPlan) => void; + loading?: boolean; + className?: string; +} + +export function DataPlanSelect({ + dataPlans, + value, + onSelect, + loading = false, + className = "", +}: DataPlanSelectProps) { + if (loading) { + return ( +
+ +
+ {[...Array(4)].map((_, i) => ( +
+ ))} +
+
+ ); + } + + if (dataPlans.length === 0) { + return ( +
+ +
+ No data plans available for this network +
+
+ ); + } + + const formatAmount = (amount: string) => { + const numAmount = parseInt(amount.replace(/[N₦,]/g, ""), 10); + return `₦${numAmount.toLocaleString()}`; + }; + + return ( +
+ +
+ {dataPlans.map((plan) => ( +
+ onSelect(plan)} + className={`w-full p-4 rounded-xl text-left transition-all ${ + value?.dataplanId === plan.dataplanId + ? "bg-primary/10" + : "bg-card hover:bg-accent border border-border" + }`} + > +
+
+

{plan.name}

+

+ {plan.validity} +

+ {plan.description && ( +

+ {plan.description} +

+ )} +
+
+ + {formatAmount(plan.amount)} + +
+
+
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/components/dashboard/purchase/PurchaseCommon/MeterTypeSelect.tsx b/components/dashboard/purchase/PurchaseCommon/MeterTypeSelect.tsx new file mode 100644 index 0000000..19256bb --- /dev/null +++ b/components/dashboard/purchase/PurchaseCommon/MeterTypeSelect.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { motion } from "framer-motion"; + +interface MeterType { + value: "prepaid" | "postpaid"; + label: string; +} + +interface MeterTypeSelectProps { + meterTypes: MeterType[]; + value: "prepaid" | "postpaid"; + onChange: (type: "prepaid" | "postpaid") => void; + className?: string; +} + +export function MeterTypeSelect({ + meterTypes, + value, + onChange, + className = "", +}: MeterTypeSelectProps) { + if (meterTypes.length === 0) { + return null; + } + + return ( +
+ +
+ {meterTypes.map((type) => ( + onChange(type.value)} + className={`p-4 rounded-xl transition-all border ${ + value === type.value + ? "border-primary bg-primary/10 text-primary font-semibold" + : "border-border hover:border-primary/50 hover:bg-accent" + }`} + > +
+
{type.label}
+
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/components/dashboard/purchase/PurchaseCommon/ProviderSelect.tsx b/components/dashboard/purchase/PurchaseCommon/ProviderSelect.tsx new file mode 100644 index 0000000..d1aadb3 --- /dev/null +++ b/components/dashboard/purchase/PurchaseCommon/ProviderSelect.tsx @@ -0,0 +1,93 @@ +import React from "react"; +import Image from "next/image"; +import { motion } from "framer-motion"; + +interface Provider { + serviceID: string; + name: string; + image: string; + code?: string; + minAmount?: number; + maxAmount?: number; +} + +interface ProviderSelectProps { + providers: Provider[]; + value: string; + onChange: (service_id: string) => void; + loading?: boolean; + className?: string; +} + +export function ProviderSelect({ + providers, + value, + onChange, + loading = false, + className = "", +}: ProviderSelectProps) { + if (loading) { + return ( +
+ {[...Array(4)].map((_, i) => ( +
+ ))} +
+ ); + } + + if (providers.length === 0) { + return ( +
+ No providers available +
+ ); + } + + return ( +
+ {providers.map((provider) => ( + onChange(provider.serviceID)} + className={`w-full rounded-2xl transition-all ${ + value === provider.serviceID + ? " border border-border bg-primary/10 text-primary" + : " hover:border hover:border-primary/50 bg-muted/30" + }`} + style={{ minWidth: "100px" }} + > +
+
+ {provider.name} { + e.currentTarget.style.display = "none"; + const parent = e.currentTarget.parentElement; + if (parent) { + parent.innerHTML = ` +
+ ${provider.name.substring(0, 2)} +
+ `; + } + }} + /> +
+ + {provider.name} + +
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/components/dashboard/purchase/PurchaseCommon/PurchasePinDialog.tsx b/components/dashboard/purchase/PurchaseCommon/PurchasePinDialog.tsx new file mode 100644 index 0000000..0d43d8b --- /dev/null +++ b/components/dashboard/purchase/PurchaseCommon/PurchasePinDialog.tsx @@ -0,0 +1,84 @@ +import React, { useState, useRef } from "react"; +import { motion } from "framer-motion"; +import { Loader2 } from "lucide-react"; + +interface PurchasePinDialogProps { + isOpen: boolean; + onClose: () => void; + onPinComplete: (pin: string) => void; + isLoading: boolean; +} + +export function PurchasePinDialog({ + isOpen, + onClose, + onPinComplete, + isLoading, +}: PurchasePinDialogProps) { + const [pin, setPin] = useState(["", "", "", ""]); + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + const handleChange = (index: number, value: string) => { + if (value.length > 1) return; + if (value && !/^\d$/.test(value)) return; + + const newPin = [...pin]; + newPin[index] = value; + setPin(newPin); + + if (value && index < 3) { + inputRefs.current[index + 1]?.focus(); + } + + if (newPin.every((digit) => digit !== "") && index === 3) { + onPinComplete(newPin.join("")); + } + }; + + if (!isOpen) return null; + + return ( + + e.stopPropagation()} + className="bg-card border rounded-2xl p-8 max-w-md w-full mx-4" + > +

Enter PIN

+ +
+ {pin.map((digit, index) => ( + { + inputRefs.current[index] = el; + }} + type="password" + inputMode="numeric" + maxLength={1} + value={digit} + onChange={(e) => handleChange(index, e.target.value)} + disabled={isLoading} + className="w-16 h-16 text-center bg-background border-2 rounded-xl text-2xl focus:outline-none focus:border-primary" + /> + ))} +
+ + {isLoading && ( +
+ + Processing... +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/components/dashboard/purchase/PurchaseCommon/SuccessScreen.tsx b/components/dashboard/purchase/PurchaseCommon/SuccessScreen.tsx new file mode 100644 index 0000000..9164288 --- /dev/null +++ b/components/dashboard/purchase/PurchaseCommon/SuccessScreen.tsx @@ -0,0 +1,238 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { Check, X, Home, Download, Copy } from "lucide-react"; +import { PurchaseConfig } from "@/components/hooks/usePurchaseConfig"; + +interface SuccessScreenProps { + success: boolean | null; + type: "airtime" | "data" | "electricity"; + formData: any; + providers: any[]; + transactionData: any; + config: PurchaseConfig; + formatCustomerId: (id: string) => string; + onReset: () => void; + className?: string; +} + +export function SuccessScreen({ + success, + type, + formData, + providers, + transactionData, + config, + formatCustomerId, + onReset, + className = "", +}: SuccessScreenProps) { + const handleCopy = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + // You could add a toast notification here + } catch (err) { + console.error("Failed to copy:", err); + } + }; + + const handleDownload = () => { + // Implement receipt download logic + console.log("Download receipt"); + }; + + if (success === null) { + return ( +
+
Processing transaction...
+
+ ); + } + + return ( + + {/* Animated Icon */} + + {success ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ + {/* Title */} +
+

+ {success ? "Transaction Successful!" : "Transaction Failed"} +

+

+ {success + ? "Your transaction has been processed successfully" + : "Something went wrong. Please try again."} +

+
+ + {/* Transaction Details */} + {success && ( + +

Transaction Details

+ +
+
+ Amount + + ₦{parseInt(formData.amount || "0").toLocaleString()} + +
+ +
+ Provider + + { + providers.find((p) => p.serviceID === formData.service_id) + ?.name + } + +
+ + {formData.customer_id && ( +
+ + {type === "electricity" ? "Meter Number" : "Phone Number"} + + + {type === "electricity" + ? formData.customer_id + : `234${formData.customer_id}`} + +
+ )} + + {transactionData?.purchaseId && ( +
+ Transaction ID +
+ + {transactionData.purchaseId.slice(0, 8)}... + + +
+
+ )} + + {transactionData?.meterToken && ( +
+
+
+ Meter Token +

+ Use this token to recharge your meter +

+
+
+ + {transactionData.meterToken.slice(0, 12)}... + + +
+
+
+ )} +
+ + {/* Download Receipt Button */} +
+ +
+
+ )} + + {/* Action Buttons */} +
+ + + {success ? "New Purchase" : "Try Again"} + + + {success && ( + + )} +
+ + {/* Confetti Effect (Success only) */} + {success && ( +
+ {[...Array(20)].map((_, i) => ( + + ))} +
+ )} +
+ ); +} diff --git a/components/dashboard/purchase/PurchaseFlow.tsx b/components/dashboard/purchase/PurchaseFlow.tsx new file mode 100644 index 0000000..be29407 --- /dev/null +++ b/components/dashboard/purchase/PurchaseFlow.tsx @@ -0,0 +1,212 @@ +// /components/purchase/PurchaseFlow.tsx +"use client"; + +import React from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Loader2, AlertCircle } from "lucide-react"; + +// Import hooks and sub-components +import { usePurchaseFlow } from "@/components/hooks/usePurchaseFlow"; +import { usePurchaseConfig } from "@/components/hooks/usePurchaseConfig"; +import { ProviderSelection } from "./PurchaseSteps/ProviderSelection"; +import { PaymentSelection } from "./PurchaseSteps/PaymentSelection"; +import { ReviewConfirmation } from "./PurchaseSteps/ReviewConfirmation"; +import { SuccessScreen } from "./PurchaseCommon/SuccessScreen"; +import { PurchasePinDialog } from "./PurchaseCommon/PurchasePinDialog"; + +interface PurchaseProps { + type: "airtime" | "data" | "electricity"; +} + +export default function PurchaseFlow({ type }: PurchaseProps) { + const { + // State + step, + loading, + success, + showPinDialog, + isSending, + selectedToken, + toAddress, + errorMessage, + verifyingMeter, + meterVerified, + meterVerificationMessage, + formData, + providers, + dataPlans, + electricityCompanies, + meterTypes, + transactionData, + + // Handlers + setStep, + handleBack, + handleNext, + handleConfirm, + handleTokenSelect, + setFormData, + handleSendWithPin, + setShowPinDialog, + handleVerifyMeter, + resetForm, + + // Validation + isStep1Valid, + validationError, + + // Utilities + requiredCryptoAmount, + currentWalletBalance, + } = usePurchaseFlow({ type }); + + // Use the config hook + const { + config, + getPresetAmounts, + getValidationRules, + getRequiredFields, + getRecommendedTokens, + formatCustomerId, + getTransactionDescription, + getProgressPercentage, + } = usePurchaseConfig(type); + + const presetAmounts = getPresetAmounts; + + const renderStep = () => { + switch (step) { + case 1: + return ( + + ); + + case 2: + return ( + + ); + + case 3: + return ( + + ); + + case 4: + return ( + + ); + + default: + return null; + } + }; + + if (loading && step === 1) { + return ( +
+ +

Loading purchase options...

+
+ ); + } + + return ( +
+ {/* Progress Bar */} +
+
+

{config.title}

+ + Step {step} of 4 + +
+
+
+
+
+ + {/* PIN Dialog */} + setShowPinDialog(false)} + onPinComplete={handleSendWithPin} + isLoading={isSending} + /> + + + + {renderStep()} + + + + {/* Global Error Message */} + {errorMessage && ( + +
+ + {errorMessage} +
+
+ )} +
+ ); +} diff --git a/components/dashboard/purchase/PurchaseSteps/PaymentSelection.tsx b/components/dashboard/purchase/PurchaseSteps/PaymentSelection.tsx new file mode 100644 index 0000000..d38b4ef --- /dev/null +++ b/components/dashboard/purchase/PurchaseSteps/PaymentSelection.tsx @@ -0,0 +1,81 @@ +import { motion } from "framer-motion"; +import { ArrowLeft, ChevronRight } from "lucide-react"; +import WalletSAndBalance from "@/components/modals/walletSAndBalance"; + +interface PaymentSelectionProps { + type: string; + formData: any; + selectedToken: string; + providers: any[]; + config: any; + onTokenSelect: (token: string) => void; + onBack: () => void; + onNext: () => void; +} + +export function PaymentSelection({ + formData, + selectedToken, + providers, + onTokenSelect, + onBack, + onNext, +}: PaymentSelectionProps) { + // const [selectedToken, setSelectedToken] = useState(null); + + + return ( + +
+ +

Select Payment Method

+
+ +
+
+
+

Amount

+

+ ₦{parseInt(formData.amount || "0").toLocaleString()} +

+
+
+

Provider

+

+ {providers.find(p => p.serviceID === formData.service_id)?.name} +

+
+
+
+ +
+ +
+ +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/components/dashboard/purchase/PurchaseSteps/ProviderSelection.tsx b/components/dashboard/purchase/PurchaseSteps/ProviderSelection.tsx new file mode 100644 index 0000000..085bbff --- /dev/null +++ b/components/dashboard/purchase/PurchaseSteps/ProviderSelection.tsx @@ -0,0 +1,201 @@ +// /components/purchase/PurchaseSteps/ProviderSelection.tsx +import { motion } from "framer-motion"; +import { ChevronRight, AlertCircle, Loader2 } from "lucide-react"; +import { ProviderSelect } from "../PurchaseCommon/ProviderSelect"; +import { AmountGrid } from "../PurchaseCommon/AmountGrid"; +import { CustomerInput } from "../PurchaseCommon/CustomerInput"; +import { DataPlanSelect } from "../PurchaseCommon/DataPlanSelect"; +import { MeterTypeSelect } from "../PurchaseCommon/MeterTypeSelect"; + +interface ProviderSelectionProps { + type: "airtime" | "data" | "electricity"; + formData: any; + setFormData: (data: any) => void; + providers: any[]; + dataPlans: any[]; + meterTypes: any[]; + presetAmounts: number[]; + verifyingMeter: boolean; + meterVerified: boolean; + meterVerificationMessage: string; + config: any; + isStep1Valid: () => boolean; + errorMessage?: string; + loading?: boolean; + onNext: () => void; + onVerifyMeter: () => void; +} + +export function ProviderSelection({ + type, + formData, + setFormData, + providers, + dataPlans, + meterTypes, + presetAmounts, + verifyingMeter, + meterVerified, + meterVerificationMessage, + config, + isStep1Valid, + errorMessage, + loading = false, + onNext, + onVerifyMeter, +}: ProviderSelectionProps) { + const updateFormData = (field: string, value: any) => { + setFormData((prev: any) => ({ ...prev, [field]: value })); + }; + + // Get provider min/max amounts if available + const selectedProvider = providers.find(p => p.serviceID === formData.service_id); + const minAmount = selectedProvider?.minAmount; + const maxAmount = selectedProvider?.maxAmount; + + if (loading && providers.length === 0) { + return ( +
+ +

Loading providers...

+
+ ); + } + + return ( + + {/* Provider Selection */} +
+ + updateFormData("service_id", serviceId)} + loading={loading} + /> +
+ + {/* Amount Grid (for airtime/electricity) */} + {config.showAmountGrid && formData.service_id && ( + updateFormData("amount", amount)} + presetAmounts={presetAmounts} + minAmount={minAmount} + maxAmount={maxAmount} + /> + )} + + {/* Data Plan Selection (for data purchases) */} + {type === "data" && formData.service_id && ( + updateFormData("dataplan", plan)} + loading={loading} + /> + )} + + {/* Customer Input (Phone/Meter Number) */} + {formData.service_id && (config.showAmountGrid || type === "data") && ( + updateFormData("customer_id", value)} + config={config} + /> + )} + + {/* Meter Type Selection (for electricity) */} + {type === "electricity" && config.showMeterType && ( + updateFormData("meterType", type)} + /> + )} + + {/* Meter Verification (for electricity) */} + {config.showVerifyButton && formData.service_id && formData.customer_id && ( +
+ + + {meterVerificationMessage && ( +
+ {meterVerificationMessage} +
+ )} +
+ )} + + {/* Phone Number for Electricity (after meter verification) */} + {type === "electricity" && formData.customer_id && meterVerified && ( +
+ + updateFormData("phoneNo", e.target.value.replace(/[^\d]/g, ''))} + placeholder="08123456789" + className="w-full p-4 rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20" + maxLength={11} + /> +

+ Enter your phone number for notifications +

+
+ )} + + {/* Error Message */} + {errorMessage && ( +
+
+ + {errorMessage} +
+
+ )} + + {/* Continue Button */} + + Continue + + +
+ ); +} \ No newline at end of file diff --git a/components/dashboard/purchase/PurchaseSteps/ReviewConfirmation.tsx b/components/dashboard/purchase/PurchaseSteps/ReviewConfirmation.tsx new file mode 100644 index 0000000..dbfe821 --- /dev/null +++ b/components/dashboard/purchase/PurchaseSteps/ReviewConfirmation.tsx @@ -0,0 +1,135 @@ +// /components/purchase/PurchaseSteps/ReviewConfirmation.tsx +import { motion } from "framer-motion"; +import { ArrowLeft, ChevronRight, AlertCircle } from "lucide-react"; + +interface ReviewConfirmationProps { + type: string; + formData: any; + selectedToken: string; + providers: any[]; + config: any; + requiredCryptoAmount: number; + currentWalletBalance: number; + validationError: string | null; + onBack: () => void; + onConfirm: () => void; +} + +export function ReviewConfirmation({ + type, + formData, + selectedToken, + providers, + config, + requiredCryptoAmount, + currentWalletBalance, + validationError, + onBack, + onConfirm, +}: ReviewConfirmationProps) { + const hasInsufficientBalance = requiredCryptoAmount > currentWalletBalance; + + return ( + +
+ +

Transaction Summary

+
+ +
+
+ Product + {type} +
+
+ Provider + + {providers.find(p => p.serviceID === formData.service_id)?.name} + +
+
+ {config.customerLabel} + + {type !== "electricity" ? `234${formData.customer_id}` : formData.customer_id} + +
+
+ Payment Method + {selectedToken} +
+
+ Total Amount + + ₦{parseInt(formData.amount || "0").toLocaleString()} + +
+ + {/* Crypto Amount */} +
+
+ Required Crypto + + {requiredCryptoAmount.toFixed(6)} {selectedToken.toUpperCase()} + +
+
+ Your Balance + + {currentWalletBalance.toFixed(6)} {selectedToken.toUpperCase()} + +
+
+
+ + {hasInsufficientBalance && ( +
+
+ + Insufficient balance for this transaction +
+
+ )} + + {validationError && !hasInsufficientBalance && ( +
+
+ + {validationError} +
+
+ )} + +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/components/dashboard/quick-actions.tsx b/components/dashboard/quick-actions.tsx index 0c0a507..be9df0a 100644 --- a/components/dashboard/quick-actions.tsx +++ b/components/dashboard/quick-actions.tsx @@ -1,77 +1,99 @@ -import { QrCode, Users, Send, ArrowDownToLine } from "lucide-react"; +import { + QrCode, + Users, + Send, + ArrowDownToLine, + Lightbulb, + Wifi, + Phone, + ArrowUpToLine, +} from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "../ui/cards"; import { Button } from "../ui/buttons"; import { Dispatch, SetStateAction } from "react"; +import Link from "next/link"; interface quickActionProps { setTab: Dispatch>; } const actions = [ + { + title: "Airtime", + link: "/dashboard/service/airtime", + icon: Phone, + gradient: "from-accent to-secondary", + }, + { + title: "Data", + link: "/dashboard/service/data", + icon: Wifi, + gradient: "from-char-2 to-accent", + }, + { + title: "Electricity", + link: "/dashboard/service/electricity", + icon: Lightbulb, + gradient: "from-primary to-success", + }, { title: "QRPayment", - description: "Scan or generate QR codes", + link: "/dashboard/merchant", icon: QrCode, gradient: "from-primary to-accent", }, { title: "Payment split", - description: "Split payments with others", + link: "/dashboard/split", icon: Users, gradient: "from-success to-chart-2", }, { title: "Send", - description: "Transfer to any wallet", + link: "/dashboard/send", icon: Send, gradient: "from-chart-3 to-chart-4", }, { title: "Receive funds", - description: "Get paid instantly", + link: "/dashboard/receive", icon: ArrowDownToLine, - gradient: "from-accent to-primary", + gradient: "from-primary to-chart-2", }, { title: "Top Up", - description: "Buy crypto with NGN", - icon: ArrowDownToLine, + link: "/dashboard/topup", + icon: ArrowUpToLine, gradient: "from-primary to-accent", }, ]; -export function QuickActions({ setTab }: quickActionProps) { +export function QuickActions() { return ( - +
Quick Actions -
+
{actions.map((action) => { const Icon = action.icon; return ( - + ); })}
- +
); } diff --git a/components/dashboard/recent-activity.tsx b/components/dashboard/recent-activity.tsx index 43e3cb4..98b2130 100644 --- a/components/dashboard/recent-activity.tsx +++ b/components/dashboard/recent-activity.tsx @@ -4,20 +4,21 @@ import { CardHeader, CardTitle, } from "@/components/ui/cards"; -import { Button } from "@/components/ui/buttons"; import { ArrowDownLeft, ArrowUpRight, ChevronRight } from "lucide-react"; import { DashboardProps } from "./tabs/dashboard"; import { useNotifications } from "../hooks/useNotifications"; import { shortenAddress } from "../lib/utils"; +import Link from "next/link"; export function RecentActivity({ activeTab }: DashboardProps) { const { notifications } = useNotifications(); const filtered = notifications.filter((notif) => { - return notif.title === "Deposit Successful" || notif.title === "Tokens Sent"; + return ( + notif.title === "Deposit Successful" || notif.title === "Tokens Sent" + ); }); - const finalNotificationFix = filtered.slice(0, 5); return ( @@ -26,15 +27,13 @@ export function RecentActivity({ activeTab }: DashboardProps) { Recent Activity - + {finalNotificationFix.map((notification) => ( @@ -62,10 +61,10 @@ export function RecentActivity({ activeTab }: DashboardProps) {
{notification.time}
-
+
{notification.title === "Deposit Successful" && (
- {notification.details.amount} {notification.details.chain} + {notification.details.amount} {notification.details.chain}
)} @@ -79,7 +78,7 @@ export function RecentActivity({ activeTab }: DashboardProps) {
{shortenAddress(notification.details.address, 6)}
)} - {notification.title === "Tokens Sent" && ( + {notification.title === "Tokens Sent" && (
{shortenAddress(notification.details.txHash, 6)}
)}
diff --git a/components/dashboard/stats-cards.tsx b/components/dashboard/stats-cards.tsx index e977b5c..83d6f20 100644 --- a/components/dashboard/stats-cards.tsx +++ b/components/dashboard/stats-cards.tsx @@ -110,10 +110,10 @@ export function StatsCards({ }); return ( -
+
{updatedStats.map((stat, index) => ( - {stat.title === "Total Balance" && ( @@ -174,7 +174,7 @@ export function StatsCards({
- +
))}
); diff --git a/components/dashboard/tabs/dashboard.tsx b/components/dashboard/tabs/dashboard.tsx index 59d23ed..61dfe08 100644 --- a/components/dashboard/tabs/dashboard.tsx +++ b/components/dashboard/tabs/dashboard.tsx @@ -60,7 +60,7 @@ export default function DashboardHome({ activeTab }: DashboardProps) { {/* Main Content Grid */}
- +
diff --git a/components/dashboard/tabs/electricity.tsx b/components/dashboard/tabs/electricity.tsx index da29901..3dce95e 100644 --- a/components/dashboard/tabs/electricity.tsx +++ b/components/dashboard/tabs/electricity.tsx @@ -1,7 +1,7 @@ "use client" import React, { useEffect } from 'react' -import Purchase from '../service-flow' +import Purchase from '@/components/dashboard/service-flow' export default function Electricity() { diff --git a/components/dashboard/top-nav.tsx b/components/dashboard/top-nav.tsx index 734dad1..4861d4c 100644 --- a/components/dashboard/top-nav.tsx +++ b/components/dashboard/top-nav.tsx @@ -7,6 +7,7 @@ import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import { ThemeToggle } from "@/components/ui/theme-toggle"; import Notification from "@/components/ui/notification"; import { Card } from "@/components/ui/Card"; +import Link from "next/link"; // import { useDeposits } from "../hooks"; interface DashboardHeaderProps { @@ -14,11 +15,11 @@ interface DashboardHeaderProps { setTab: React.Dispatch>; } -export function TopNav({ tabTitle, setTab }: DashboardHeaderProps) { +export function TopNav() { // const { checkDeposits } = useDeposits(); -const [searchOpen, setSearchOpen] = useState(false); + const [searchOpen, setSearchOpen] = useState(false); -// Automatically start checking deposits when component mounts + // Automatically start checking deposits when component mounts // const checkRef = useRef(checkDeposits); // useEffect(() => { // checkRef.current = checkDeposits; @@ -34,61 +35,54 @@ const [searchOpen, setSearchOpen] = useState(false); // return () => window.clearInterval(id); // }, []); -; - const isHelp = tabTitle === "Help"; + // const isHelp = tabTitle === "Help"; return (
{/* Actions */} -
- {tabTitle} -
-
- {/* Desktop search */} -
- - -
- - {/* Mobile search sheet */} - - - - - -
- - -
-
-
- - +
- + {/* Desktop search */} + + + + + +
+ + +
+
+
+ +
+ + +
- - - + {/* Mobile search sheet */} +
+ +
+ +
+ + + + + +
diff --git a/components/dashboard/wallet-overview.tsx b/components/dashboard/wallet-overview.tsx index 850512e..0dfae6f 100644 --- a/components/dashboard/wallet-overview.tsx +++ b/components/dashboard/wallet-overview.tsx @@ -22,7 +22,7 @@ export function WalletOverview({ hideBalalance, }: WalletOverviewProps) { const { addresses, breakdown } = useWalletData(); - + const [copiedAddress, setCopiedAddress] = useState(null); const walletData = (() => { @@ -33,8 +33,10 @@ export function WalletOverview({ if (k === "eth" || k === "ethereum") return "ethereum"; if (k === "btc" || k === "bitcoin") return "bitcoin"; if (k === "strk" || k === "starknet") return "starknet"; - if (k === "usdt" || k === "usdt_erc20" || k === "usdt-erc20") return "usdt_erc20"; - if (k === "usdt_trc20" || k === "usdt-trc20" || k === "usdttrc20") return "usdt_trc20"; + if (k === "usdt" || k === "usdt_erc20" || k === "usdt-erc20") + return "usdt_erc20"; + if (k === "usdt_trc20" || k === "usdt-trc20" || k === "usdttrc20") + return "usdt_trc20"; if (k === "dot" || k === "polkadot") return "polkadot"; if (k === "xlm" || k === "stellar") return "stellar"; return k; @@ -102,45 +104,39 @@ export function WalletOverview({ ); return ( - - {/* - - Wallet Overview - - - */} - - + // + + {walletData?.map((wallet, index) => (
-
-
- {wallet.chain} { - (e.target as HTMLImageElement).style.display = "none"; - ( - e.target as HTMLImageElement - ).nextElementSibling?.classList.remove("hidden"); - }} - /> - - {wallet.chain.charAt(0)} - +
+
+
+ {wallet.chain} { + (e.target as HTMLImageElement).style.display = "none"; + ( + e.target as HTMLImageElement + ).nextElementSibling?.classList.remove("hidden"); + }} + /> +
+ {walletData.length === 0 ? ( + + ) : ( +

+ {formatBalance(wallet.balance, wallet.symbol)} +

+ )}
+
{/*

{wallet.chain} @@ -148,13 +144,6 @@ export function WalletOverview({

{shortenAddress(wallet.address as `0x${string}`, 6)}

*/} - {walletData.length === 0 ? ( - - ) : ( -

- {formatBalance(wallet.balance, wallet.symbol)} -

- )}
@@ -173,15 +162,12 @@ export function WalletOverview({ {formatNGN(wallet.ngnValue)}

)} - )}
))} - - - + // ); -} \ No newline at end of file +} diff --git a/components/hooks/useAddresses.tsx b/components/hooks/useAddresses.tsx deleted file mode 100644 index e1f6095..0000000 --- a/components/hooks/useAddresses.tsx +++ /dev/null @@ -1,32 +0,0 @@ -// import { useState, useEffect } from 'react'; -// import { useAuth } from '../context/AuthContext'; - -// export const useWalletAddresses = () => { -// const { getWalletAddresses, token } = useAuth(); -// const [addresses, setAddresses] = useState([]); -// const [loading, setLoading] = useState(false); -// const [error, setError] = useState(null); - -// const fetchAddresses = async () => { -// if (!token) return; - -// setLoading(true); -// setError(null); - -// try { -// const walletAddresses = await getWalletAddresses(); -// setAddresses(walletAddresses || []); -// } catch (err) { -// setError(err instanceof Error ? err.message : 'Failed to fetch addresses'); -// } finally { -// setLoading(false); -// } -// }; - -// useEffect(() => { -// fetchAddresses(); -// }, [token]); - - -// return { addresses, loading, error, refetch: fetchAddresses }; -// }; \ No newline at end of file diff --git a/components/hooks/useApiQuery.ts b/components/hooks/useApiQuery.ts new file mode 100644 index 0000000..3bd7411 --- /dev/null +++ b/components/hooks/useApiQuery.ts @@ -0,0 +1,132 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import { apiClient } from '@/lib/api-client'; +import { StorageManager, getMemoryCache, setMemoryCache } from '@/lib/utils/storage-utils'; +import { useAuth } from '@/components/context/AuthContext'; + +interface UseApiQueryOptions { + ttl?: number; + backgroundRefresh?: boolean; + cacheKey: string; + requireAuth?: boolean; +} + +export function useApiQuery( + fetchFn: () => Promise, + options: UseApiQueryOptions +) { + const { + ttl = 5 * 60 * 1000, + backgroundRefresh = true, + cacheKey, + requireAuth = true, + } = options; + + const { token } = useAuth(); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const mountedRef = useRef(true); + const fetchFnRef = useRef(fetchFn); + + useEffect(() => { + fetchFnRef.current = fetchFn; + }, [fetchFn]); + + useEffect(() => { + return () => { + mountedRef.current = false; + }; + }, []); + + const fetchData = useCallback( + async (isBackgroundRefresh = false) => { + if (requireAuth && !token) { + if (!isBackgroundRefresh) { + setError('Authentication required'); + setIsLoading(false); + } + return; + } + + if (!isBackgroundRefresh) { + setIsLoading(true); + } + + try { + const result = await fetchFnRef.current(); + + // Update caches + setMemoryCache(cacheKey, result, ttl); + StorageManager.set(cacheKey, result); + + if (!mountedRef.current) return; + + setData(result); + setError(null); + } catch (err) { + if (!mountedRef.current) return; + + if (!isBackgroundRefresh) { + setError(err instanceof Error ? err.message : 'Failed to fetch data'); + } + console.error(`Error fetching ${cacheKey}:`, err); + } finally { + if (!isBackgroundRefresh) { + setIsLoading(false); + } + } + }, + [token, cacheKey, ttl, requireAuth] + ); + + const initializeData = useCallback(async () => { + // Try memory cache first + const memoryCached = getMemoryCache(cacheKey); + if (memoryCached) { + setData(memoryCached); + setIsLoading(false); + + if (backgroundRefresh) { + setTimeout(() => fetchData(true), 2000); + } + return; + } + + // Try storage cache + const storageCached = StorageManager.get(cacheKey); + if (storageCached) { + setData(storageCached); + setIsLoading(false); + + if (backgroundRefresh) { + setTimeout(() => fetchData(true), 2000); + } + return; + } + + // No cache - fetch fresh + await fetchData(false); + }, [cacheKey, fetchData, backgroundRefresh]); + + useEffect(() => { + initializeData(); + }, [initializeData]); + + useEffect(() => { + if (!backgroundRefresh) return; + + const interval = setInterval(() => { + if (!document.hidden) { + fetchData(true); + } + }, 30000); + + return () => clearInterval(interval); + }, [fetchData, backgroundRefresh]); + + const refetch = useCallback(async () => { + await fetchData(false); + }, [fetchData]); + + return { data, error, isLoading, refetch }; +} \ No newline at end of file diff --git a/components/hooks/useAuthQuery.ts b/components/hooks/useAuthQuery.ts new file mode 100644 index 0000000..658a5c2 --- /dev/null +++ b/components/hooks/useAuthQuery.ts @@ -0,0 +1,17 @@ +import { useApiQuery } from './useApiQuery'; +import { useAuth } from '@/components/context/AuthContext'; + +export function useAuthQuery( + fetchFn: () => Promise, + options: Omit[1], 'requireAuth'> +) { + const { token } = useAuth(); + + return useApiQuery(async () => { + if (!token) throw new Error('Authentication required'); + return fetchFn(); + }, { + ...options, + requireAuth: true, + }); +} \ No newline at end of file diff --git a/components/hooks/useBack.ts b/components/hooks/useBack.ts new file mode 100644 index 0000000..7a72ddb --- /dev/null +++ b/components/hooks/useBack.ts @@ -0,0 +1,22 @@ +"use client"; +import { useRouter } from "next/navigation"; + +/** + * Returns a function that navigates back to the previous page. + * Falls back to a default URL if there is no history. + * + * @param fallbackUrl - URL to navigate to if there is no previous page + */ +export const useBack = (fallbackUrl: string = "/") => { + const router = useRouter(); + + const goBack = () => { + if (window.history.length > 1) { + router.back(); + } else { + router.push(fallbackUrl); + } + }; + + return goBack; +}; diff --git a/components/hooks/useCachedQuery.ts b/components/hooks/useCachedQuery.ts deleted file mode 100644 index c80257f..0000000 --- a/components/hooks/useCachedQuery.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { apiClient } from '@/lib/api-client'; - -interface UseCachedQueryOptions { - ttl?: number; // Cache time-to-live in ms - backgroundRefresh?: boolean; -} - -export function useCachedQuery( - queryKey: string, - fetchFn: () => Promise, - options: UseCachedQueryOptions = {} -) { - const { ttl = 5 * 60 * 1000, backgroundRefresh = true } = options; - - const [data, setData] = useState(null); - const [error, setError] = useState(null); - const [isInitializing, setIsInitializing] = useState(true); - - const fetchData = useCallback(async (isBackgroundRefresh = false) => { - if (!isBackgroundRefresh) { - setIsInitializing(true); - } - - try { - // Try cache first (apiClient handles this internally) - const result = await fetchFn(); - setData(result); - setError(null); - } catch (err) { - if (!isBackgroundRefresh) { - setError(err instanceof Error ? err.message : 'Failed to fetch data'); - } - console.error(`Error fetching ${queryKey}:`, err); - } finally { - if (!isBackgroundRefresh) { - setIsInitializing(false); - } - } - }, [fetchFn, queryKey]); - - // Initial load - silent if cache exists - useEffect(() => { - const initializeData = async () => { - // Check if we have cached data - const cachedData = apiClient.getCachedData(queryKey); - - if (cachedData) { - setData(cachedData); - setIsInitializing(false); - - // Still refresh in background if data might be stale - if (backgroundRefresh) { - fetchData(true); - } - } else { - // No cache, need to fetch - await fetchData(false); - } - }; - - initializeData(); - }, [fetchData, queryKey, backgroundRefresh]); - - // Background refresh interval - useEffect(() => { - if (!backgroundRefresh) return; - - const interval = setInterval(() => { - if (!document.hidden) { - fetchData(true); - } - }, 30000); // Every 30 seconds - - return () => clearInterval(interval); - }, [fetchData, backgroundRefresh]); - - const refetch = useCallback(async () => { - await fetchData(false); - }, [fetchData]); - - return { - data, - error, - isInitializing, // Only true on very first load when no cache exists - refetch, - }; -} \ No newline at end of file diff --git a/components/hooks/useCommonOperations.ts b/components/hooks/useCommonOperations.ts new file mode 100644 index 0000000..768af8c --- /dev/null +++ b/components/hooks/useCommonOperations.ts @@ -0,0 +1,42 @@ +import { useCallback } from 'react'; +import { useAuth } from '@/components/context/AuthContext'; +import { normalizeChain, getTokenSymbol } from '@/lib/utils/token-utils'; + +export const useCommonOperations = () => { + const { token } = useAuth(); + + const requireAuth = useCallback(() => { + if (!token) throw new Error('Authentication required'); + return token; + }, [token]); + + const formatBalance = useCallback((balance: number): string => { + if (balance === 0) return "0.00"; + if (balance < 0.001) return "<0.001"; + return balance.toFixed(4); + }, []); + + const formatNGN = useCallback((amount: number): string => { + return new Intl.NumberFormat("en-NG", { + style: "currency", + currency: "NGN", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(amount); + }, []); + + const shortenAddress = useCallback((address: string, chars = 8): string => { + if (!address || address.length <= chars * 2) return address; + return `${address.slice(0, chars)}...${address.slice(-chars)}`; + }, []); + + return { + requireAuth, + formatBalance, + formatNGN, + shortenAddress, + normalizeChain, + getTokenSymbol, + isAuthenticated: !!token, + }; +}; \ No newline at end of file diff --git a/components/hooks/useExchangeRate.tsx b/components/hooks/useExchangeRate.tsx index 169caec..3159a4a 100644 --- a/components/hooks/useExchangeRate.tsx +++ b/components/hooks/useExchangeRate.tsx @@ -9,7 +9,7 @@ type Rates = { BTC: number | null; SOL: number | null; DOT: number | null; - XML: number | null; + XLM: number | null; [key: string]: number | null; }; @@ -19,7 +19,7 @@ const MAX_RETRIES = 3; export default function useExchangeRates() { const [rates, setRates] = useState({ - USDT: 1, USDC: 1, STRK: 1, SOL: 1, ETH: 1, BTC: 1, DOT: 1, XML: 1, + USDT: 1, USDC: 1, STRK: 1, SOL: 1, ETH: 1, BTC: 1, DOT: 1, XLM: 1, }); const [lastUpdated, setLastUpdated] = useState(null); const [retryCount, setRetryCount] = useState(0); @@ -42,7 +42,6 @@ export default function useExchangeRates() { setIsLoading(true); try { - console.log('Fetching exchange rates...'); const response = await fetch(API_ENDPOINT, { headers: { @@ -56,7 +55,6 @@ export default function useExchangeRates() { } const data = await response.json(); - console.log(data) if (data.error) { throw new Error(data.error); } @@ -69,14 +67,13 @@ export default function useExchangeRates() { ETH: data.ethereum?.ngn ?? 1, SOL: data.solana?.ngn ?? 1, DOT: data.polkadot?.ngn ?? 1, - XML: data.stellar?.ngn ?? 1, + XLM: data.stellar?.ngn ?? 1, }; setRates(newRates); setLastUpdated(new Date()); setRetryCount(0); setError(null); - console.log('Rates fetched successfully'); } catch (err) { console.error("Rate fetch error:", err); const newRetryCount = retryCount + 1; @@ -94,7 +91,6 @@ export default function useExchangeRates() { fetchRates(); - // Set up interval - use fixed interval, not dynamic based on retryCount const intervalId = setInterval(() => { fetchRates(); }, REFETCH_INTERVAL); diff --git a/components/hooks/useMerchantPayments.ts b/components/hooks/useMerchantPayments.ts index 4edee9b..4a19a51 100644 --- a/components/hooks/useMerchantPayments.ts +++ b/components/hooks/useMerchantPayments.ts @@ -1,244 +1,49 @@ -import { useState, useCallback, useEffect } from 'react'; +import { useAuthQuery } from './useAuthQuery'; import { apiClient } from '@/lib/api-client'; -import { useAuth } from '@/components/context/AuthContext'; -import { - CreateMerchantPaymentRequest, - CreateMerchantPaymentResponse, - GetMerchantPaymentStatusResponse, - GetMerchantPaymentHistoryResponse, - PayMerchantInvoiceResponse -} from '@/types/authContext'; +import { useCommonOperations } from './useCommonOperations'; -interface MerchantPaymentStats { - total: number; - pending: number; - completed: number; - cancelled: number; - totalAmount: string; -} +export const useMerchantPayments = () => { + const { requireAuth } = useCommonOperations(); -interface UseMerchantPaymentsReturn { - // Data - paymentHistory: GetMerchantPaymentHistoryResponse['payments']; - stats: MerchantPaymentStats | null; - - // State - isLoading: boolean; // Only true on very first load with no cache - error: string | null; - - // Actions - createPayment: (request: CreateMerchantPaymentRequest) => Promise; - getPaymentStatus: (paymentId: string) => Promise; - payInvoice: (paymentId: string, fromAddress: string) => Promise; - fetchPaymentHistory: (params?: any) => Promise; - fetchStats: () => Promise; - refetchAll: () => Promise; -} + const { data: historyData, ...historyRest } = useAuthQuery( + () => apiClient.getMerchantPaymentHistory(), + { cacheKey: 'merchant-payment-history' } + ); -export const useMerchantPayments = (): UseMerchantPaymentsReturn => { - const { token } = useAuth(); - const [paymentHistory, setPaymentHistory] = useState([]); - const [stats, setStats] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [isInitialLoad, setIsInitialLoad] = useState(true); + const { data: statsData, ...statsRest } = useAuthQuery( + () => apiClient.getMerchantPaymentStats(), + { cacheKey: 'merchant-payment-stats' } + ); - // Silent background fetch for payment history - const silentFetchPaymentHistory = useCallback(async (params?: any) => { - if (!token) return; - - try { - const response = await apiClient.getMerchantPaymentHistory(params); - setPaymentHistory(response.payments || []); - } catch (err) { - console.error('Silent background payment history fetch failed:', err); - // Don't set error for background failures - } - }, [token]); - - // Silent background fetch for stats - const silentFetchStats = useCallback(async () => { - if (!token) return; - - try { - const response = await apiClient.getMerchantPaymentStats(); - setStats(response.stats); - } catch (err) { - console.error('Silent background stats fetch failed:', err); - // Don't set error for background failures - } - }, [token]); - - // Initial fetch with cache check for payment history - const initialFetchPaymentHistory = useCallback(async (params?: any) => { - if (!token) { - setError('Authentication required'); - return; - } - - try { - // Check if we have cached payment history - const cachedHistory = apiClient.getCachedData('merchant-payment-history'); - - // If we have cached data, show it immediately - if (cachedHistory) { - setPaymentHistory(cachedHistory); - - // Still fetch fresh data in background - silentFetchPaymentHistory(params); - return; - } - - // No cache - fetch fresh data - const response = await apiClient.getMerchantPaymentHistory(params); - setPaymentHistory(response.payments || []); - } catch (err) { - console.error('Error fetching payment history:', err); - setError(err instanceof Error ? err.message : 'Failed to fetch payment history'); - } - }, [token, silentFetchPaymentHistory]); - - // Initial fetch with cache check for stats - const initialFetchStats = useCallback(async () => { - if (!token) return; - - try { - // Check if we have cached stats - const cachedStats = apiClient.getCachedData('merchant-payment-stats'); - - // If we have cached data, show it immediately - if (cachedStats) { - setStats(cachedStats); - - // Still fetch fresh data in background - silentFetchStats(); - return; - } - - // No cache - fetch fresh data - const response = await apiClient.getMerchantPaymentStats(); - setStats(response.stats); - } catch (err) { - console.error('Error fetching merchant stats:', err); - // Don't set initial error for stats - it's less critical - } - }, [token, silentFetchStats]); - - // Manual fetch for payment history (shows loading if user triggers) - const fetchPaymentHistory = useCallback(async (params?: any) => { - if (!token) { - setError('Authentication required'); - return; - } - - try { - setIsLoading(true); - await silentFetchPaymentHistory(params); - } catch (err) { - console.error('Manual payment history fetch failed:', err); - setError(err instanceof Error ? err.message : 'Failed to fetch payment history'); - } finally { - setIsLoading(false); - } - }, [token, silentFetchPaymentHistory]); - - // Manual fetch for stats - const fetchStats = useCallback(async () => { - if (!token) return; - - try { - await silentFetchStats(); - } catch (err) { - console.error('Manual stats fetch failed:', err); - // Don't set error for manual stats fetch - } - }, [token, silentFetchStats]); - - // Action methods (unchanged - these are user-initiated) - const createPayment = useCallback(async (request: CreateMerchantPaymentRequest) => { - if (!token) throw new Error('Authentication required'); + const createPayment = async (request: any) => { + requireAuth(); return await apiClient.createMerchantPayment(request); - }, [token]); + }; - const getPaymentStatus = useCallback(async (paymentId: string) => { - if (!token) throw new Error('Authentication required'); + const getPaymentStatus = async (paymentId: string) => { + requireAuth(); return await apiClient.getMerchantPaymentStatus(paymentId); - }, [token]); + }; - const payInvoice = useCallback(async (paymentId: string, fromAddress: string) => { - if (!token) throw new Error('Authentication required'); + const payInvoice = async (paymentId: string, fromAddress: string) => { + requireAuth(); return await apiClient.payMerchantInvoice(paymentId, fromAddress); - }, [token]); - - // Manual refetch all (shows loading if user triggers) - const refetchAll = useCallback(async () => { - if (!token) return; - - try { - setIsLoading(true); - await Promise.all([ - silentFetchPaymentHistory(), - silentFetchStats() - ]); - } catch (err) { - console.error('Manual refetch all failed:', err); - setError(err instanceof Error ? err.message : 'Failed to refetch merchant data'); - } finally { - setIsLoading(false); - } - }, [token, silentFetchPaymentHistory, silentFetchStats]); - - // Initial load - check cache first - useEffect(() => { - if (token) { - const initializeData = async () => { - try { - setIsLoading(true); - await Promise.all([ - initialFetchPaymentHistory(), - initialFetchStats() - ]); - } catch (err) { - console.error('Initial merchant data load failed:', err); - setError(err instanceof Error ? err.message : 'Failed to load merchant data'); - } finally { - setIsLoading(false); - setIsInitialLoad(false); - } - }; - - initializeData(); - } - }, [token, initialFetchPaymentHistory, initialFetchStats]); - - // Setup silent background refresh (every 5 minutes - merchant data changes less frequently) - useEffect(() => { - if (!token || isInitialLoad) return; - - const interval = setInterval(() => { - // Only refresh if tab is visible - if (!document.hidden) { - silentFetchPaymentHistory(); - silentFetchStats(); - } - }, 5 * 60 * 1000); // 5 minutes - - return () => clearInterval(interval); - }, [token, silentFetchPaymentHistory, silentFetchStats, isInitialLoad]); + }; - // Only show loading on very first load when no cache exists - const shouldShowLoading = isLoading && isInitialLoad; + const refetchAll = async () => { + await Promise.all([historyRest.refetch(), statsRest.refetch()]); + }; return { - paymentHistory, - stats, - isLoading: shouldShowLoading, - error, + paymentHistory: historyData?.payments || [], + stats: statsData?.stats || null, + isLoading: historyRest.isLoading || statsRest.isLoading, + error: historyRest.error || statsRest.error, createPayment, getPaymentStatus, payInvoice, - fetchPaymentHistory, - fetchStats, + fetchPaymentHistory: historyRest.refetch, + fetchStats: statsRest.refetch, refetchAll, }; }; \ No newline at end of file diff --git a/components/hooks/useNotifications.ts b/components/hooks/useNotifications.ts index de4346a..cea2bfd 100644 --- a/components/hooks/useNotifications.ts +++ b/components/hooks/useNotifications.ts @@ -3,7 +3,7 @@ import { apiClient } from "@/lib/api-client"; import { useAuth } from "@/components/context/AuthContext"; import { useToastNotifications } from "./useToastNotifications "; import { BackendNotification, FrontendNotification } from "@/types/index"; -import { useSilentQuery } from "./useSilentQuery"; +import { useApiQuery } from "./useApiQuery"; // Type guard to check if a string is a valid category const isValidCategory = ( @@ -75,7 +75,7 @@ export const useNotifications = () => { data: notificationsData, error: notificationsError, refetch: refetchNotifications - } = useSilentQuery( + } = useApiQuery( () => apiClient.getNotifications({ page: 1, limit: 1000 }), { cacheKey: 'notifications-all', @@ -88,7 +88,7 @@ export const useNotifications = () => { const { data: unreadCountData, refetch: refetchUnreadCount - } = useSilentQuery( + } = useApiQuery( () => apiClient.getUnreadCount(), { cacheKey: 'notifications-unread-count', diff --git a/components/hooks/usePin.tsx b/components/hooks/usePin.tsx index f29c880..c3b0a39 100644 --- a/components/hooks/usePin.tsx +++ b/components/hooks/usePin.tsx @@ -1,7 +1,7 @@ import { useCallback, useMemo, useState } from 'react'; import { apiClient } from '@/lib/api-client'; import { useAuth } from '@/components/context/AuthContext'; -import { useSilentQuery } from './useSilentQuery'; +import { useApiQuery } from './useApiQuery'; import { toast } from 'sonner'; interface PinResponse { @@ -40,7 +40,7 @@ export const usePin = (): UsePinReturn => { }); // Check PIN status from user profile - const { refetch: refetchProfile } = useSilentQuery( + const { refetch: refetchProfile } = useApiQuery( async (): Promise<{ hasTransactionPin?: boolean; [key: string]: any }> => { try { const profile = await apiClient.getUserProfile(); diff --git a/components/hooks/usePurchaseConfig.ts b/components/hooks/usePurchaseConfig.ts new file mode 100644 index 0000000..10fc12b --- /dev/null +++ b/components/hooks/usePurchaseConfig.ts @@ -0,0 +1,214 @@ +import { useCallback, useMemo } from "react"; +import { PhoneCall, Wifi, Zap } from "lucide-react"; + +export interface PurchaseConfig { + title: string; + icon: any; // React component + step1Title: string; + step1Description: string; + customerLabel: string; + placeholder: string; + showAmountGrid: boolean; + showVariations: boolean; + showMeterType: boolean; + showVerifyButton: boolean; + receiptFields: string[]; +} + +export function usePurchaseConfig(type: "airtime" | "data" | "electricity") { + const config = useMemo((): PurchaseConfig => { + const baseConfigs = { + airtime: { + title: "Buy Airtime", + icon: PhoneCall, + step1Title: "Select network", + step1Description: "Choose your network provider", + customerLabel: "Phone Number", + placeholder: "8012345678", + showAmountGrid: true, + showVariations: false, + showMeterType: false, + showVerifyButton: false, + receiptFields: ["Network", "Phone Number", "Amount", "Transaction ID"], + }, + data: { + title: "Purchase Data", + icon: Wifi, + step1Title: "Select network", + step1Description: "Choose your network provider", + customerLabel: "Phone Number", + placeholder: "8012345678", + showAmountGrid: false, + showVariations: true, + showMeterType: false, + showVerifyButton: false, + receiptFields: ["Network", "Phone Number", "Data Plan", "Transaction ID"], + }, + electricity: { + title: "Electricity Bill", + icon: Zap, + step1Title: "Select provider", + step1Description: "Choose your electricity provider", + customerLabel: "Meter Number", + placeholder: "Enter meter number", + showAmountGrid: true, + showVariations: false, + showMeterType: true, + showVerifyButton: true, + receiptFields: ["Provider", "Meter Number", "Amount", "Token", "Transaction ID"], + }, + }; + + return baseConfigs[type]; + }, [type]); + + // Helper function to get preset amounts based on type + const getPresetAmounts = useMemo(() => { + const typePresets = { + airtime: [100, 200, 500, 1000, 2000, 5000], + data: [100, 200, 500, 1000, 2000, 5000], // Not used for data, but available + electricity: [1000, 2000, 5000, 10000, 20000, 50000], + }; + return typePresets[type]; + }, [type]); + + // Get validation rules for customer input + const getValidationRules = useMemo(() => { + const rules = { + airtime: { + pattern: /^[0-9]{10}$/, + errorMessage: "Please enter a valid 10-digit phone number", + }, + data: { + pattern: /^[0-9]{10}$/, + errorMessage: "Please enter a valid 10-digit phone number", + }, + electricity: { + pattern: /^[0-9]{6,20}$/, + errorMessage: "Please enter a valid meter number", + }, + }; + return rules[type]; + }, [type]); + + // Get required fields for step 1 + const getRequiredFields = useMemo(() => { + const required = { + airtime: ["service_id", "amount", "customer_id"], + data: ["service_id", "dataplan", "customer_id"], + electricity: ["service_id", "amount", "customer_id", "meterType"], + }; + return required[type]; + }, [type]); + + // Get crypto token recommendations based on type + const getRecommendedTokens = useMemo(() => { + const recommendations = { + airtime: ["ethereum", "bitcoin", "usdt-erc20"], + data: ["ethereum", "solana", "usdt-erc20"], + electricity: ["ethereum", "starknet", "usdt-erc20"], + }; + return recommendations[type]; + }, [type]); + + // Format customer ID for display + const formatCustomerId = useCallback((customerId: string): string => { + if (type === "electricity") { + return customerId; + } + // Add +234 prefix for phone numbers + return `+234 ${customerId.slice(0, 3)} ${customerId.slice(3, 6)} ${customerId.slice(6)}`; + }, [type]); + + // Get transaction description + const getTransactionDescription = useCallback( + (providerName: string, amount: string, customerId: string): string => { + const formattedCustomerId = formatCustomerId(customerId); + + const descriptions = { + airtime: `${providerName} airtime recharge of ₦${amount} for ${formattedCustomerId}`, + data: `${providerName} data purchase for ${formattedCustomerId}`, + electricity: `${providerName} electricity bill payment of ₦${amount} for meter ${formattedCustomerId}`, + }; + + return descriptions[type]; + }, + [type, formatCustomerId] + ); + + // Get provider image path + const getProviderImagePath = useCallback((providerCode: string): string => { + const basePath = "/img/providers"; + const images = { + airtime: `${basePath}/telco/${providerCode.toLowerCase()}.png`, + data: `${basePath}/telco/${providerCode.toLowerCase()}.png`, + electricity: `${basePath}/power/${providerCode.toLowerCase()}.png`, + }; + return images[type]; + }, [type]); + + // Check if amount is within provider limits + const isAmountValid = useCallback( + (amount: number, provider?: { minAmount?: number; maxAmount?: number }): { + valid: boolean; + error?: string; + } => { + if (!amount || amount <= 0) { + return { valid: false, error: "Amount must be greater than 0" }; + } + + if (provider?.minAmount && amount < provider.minAmount) { + return { + valid: false, + error: `Minimum amount is ₦${provider.minAmount.toLocaleString()}`, + }; + } + + if (provider?.maxAmount && amount > provider.maxAmount) { + return { + valid: false, + error: `Maximum amount is ₦${provider.maxAmount.toLocaleString()}`, + }; + } + + return { valid: true }; + }, + [] + ); + + // Get step titles for the entire flow + const getStepTitles = useMemo(() => { + const steps = { + 1: config.step1Title, + 2: "Select Payment Method", + 3: "Review & Confirm", + 4: "Transaction Result", + }; + return steps; + }, [config.step1Title]); + + // Get progress percentage + const getProgressPercentage = useCallback((step: number): number => { + const percentages: Record = { + 1: 25, + 2: 50, + 3: 75, + 4: 100, + }; + return percentages[step] || 0; + }, []); + + return { + config, + getPresetAmounts, + getValidationRules, + getRequiredFields, + getRecommendedTokens, + formatCustomerId, + getTransactionDescription, + getProviderImagePath, + isAmountValid, + getStepTitles, + getProgressPercentage, + }; +} \ No newline at end of file diff --git a/components/hooks/usePurchaseFlow.ts b/components/hooks/usePurchaseFlow.ts new file mode 100644 index 0000000..02ef68f --- /dev/null +++ b/components/hooks/usePurchaseFlow.ts @@ -0,0 +1,750 @@ +// /components/purchase/hooks/usePurchaseFlow.ts +import { useState, useEffect, useCallback, useMemo } from "react"; +import { useAuth } from "@/components/context/AuthContext"; +import { useWalletData } from "@/components/hooks"; +import useExchangeRates from "@/components/hooks/useExchangeRate"; +import { normalizeStarknetAddress } from "@/components/lib/utils"; +import { apiClient } from "@/lib/api-client"; +import { validatePhoneNumber } from "@/lib/utils"; + +export interface PurchaseFormData { + service_id: string; + amount: string; + customer_id: string; + meterType: "prepaid" | "postpaid"; + dataplan: any | null; + expectedAmount: any | null; + transactionData: any | null; + phoneNo: string; +} + +interface Provider { + serviceID: string; + name: string; + image: string; + code?: string; + minAmount?: number; + maxAmount?: number; +} + +interface ExpectedAmount { + cryptoAmount: number; + cryptoCurrency: string; + chain: string; +} + +const presetAmounts = [100, 200, 500, 1000, 2000, 5000]; + +const getConfig = (type: "airtime" | "data" | "electricity") => { + const config = { + airtime: { + title: "Buy Airtime", + step1Title: "Select network", + step1Description: "Choose your network provider", + customerLabel: "Phone Number", + placeholder: "8012345678", + showAmountGrid: true, + showVariations: false, + showMeterType: false, + showVerifyButton: false, + }, + data: { + title: "Purchase Data", + step1Title: "Select network", + step1Description: "Choose your network provider", + customerLabel: "Phone Number", + placeholder: "8012345678", + showAmountGrid: false, + showVariations: true, + showMeterType: false, + showVerifyButton: false, + }, + electricity: { + title: "Electricity Bill", + step1Title: "Select provider", + step1Description: "Choose your electricity provider", + customerLabel: "Meter Number", + placeholder: "Enter meter number", + showAmountGrid: true, + showVariations: false, + showMeterType: true, + showVerifyButton: true, + }, + }; + return config[type]; +}; + +export function usePurchaseFlow({ type }: { type: "airtime" | "data" | "electricity" }) { + const [step, setStep] = useState(1); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(null); + const [showPinDialog, setShowPinDialog] = useState(false); + const [isSending, setIsSending] = useState(false); + const [selectedToken, setSelectedToken] = useState("ethereum"); + const [toAddress, setToAddress] = useState(""); + const [txHash, setTxHash] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + const [merchantFallback, setMerchantFallback] = useState(false); + const [verifyingMeter, setVerifyingMeter] = useState(false); + const [meterVerified, setMeterVerified] = useState(false); + const [meterVerificationMessage, setMeterVerificationMessage] = useState(""); + const [transactionData, setTransactionData] = useState(null); + + const [formData, setFormData] = useState({ + service_id: "", + amount: "", + customer_id: "", + meterType: "prepaid", + dataplan: null, + expectedAmount: null, + transactionData: null, + phoneNo: "", + }); + + const [providers, setProviders] = useState([]); + const [dataPlans, setDataPlans] = useState([]); + const [electricityCompanies, setElectricityCompanies] = useState([]); + const [meterTypes, setMeterTypes] = useState([]); + + const { sendTransaction } = useAuth(); + const { rates } = useExchangeRates(); + const { addresses, balances } = useWalletData(); + + const config = useMemo(() => getConfig(type), [type]); + + // Get merchant wallet address for selected token + const getToAddress = useCallback((chain: string) => { + if (typeof window !== "undefined") { + const runtime: any = (window as any).__VELO_MERCHANT_WALLETS; + if (runtime && typeof runtime === "object" && runtime[chain]) { + return String(runtime[chain]); + } + } + + const walletMap: { [key: string]: string | undefined } = { + ethereum: process.env.NEXT_PUBLIC_ETH_WALLET, + bitcoin: process.env.NEXT_PUBLIC_BTC_WALLET, + solana: process.env.NEXT_PUBLIC_SOL_WALLET, + stellar: process.env.NEXT_PUBLIC_XLM_WALLET, + polkadot: process.env.NEXT_PUBLIC_DOT_WALLET, + starknet: process.env.NEXT_PUBLIC_STRK_WALLET, + "usdt-erc20": process.env.NEXT_PUBLIC_USDT_WALLET, + }; + return walletMap[chain] || ""; + }, []); + + // Fetch providers based on type + const fetchProviders = useCallback(async () => { + setLoading(true); + try { + if (type === "electricity") { + const { companies } = await apiClient.getElectricitySupportedOptions(); + setElectricityCompanies(companies); + + const mappedProviders: Provider[] = companies.map((company) => ({ + serviceID: company.value, + name: company.label, + image: `/img/${company.value}.png`, + code: company.code, + minAmount: company.minAmount, + maxAmount: company.maxAmount, + })); + setProviders(mappedProviders); + } else { + const networks = + type === "data" + ? await apiClient.getDataSupportedNetworks() + : await apiClient.getAirtimeSupportedNetworks(); + + const mappedProviders: Provider[] = networks.map((network) => ({ + serviceID: network.value, + name: network.label, + image: `/img/${network.value.toLowerCase()}.png`, + })); + setProviders(mappedProviders); + } + } catch (error) { + console.error("Failed to fetch providers:", error); + setErrorMessage("Failed to load providers. Please try again."); + } finally { + setLoading(false); + } + }, [type]); + + // Fetch data plans for selected network + const fetchDataPlans = useCallback(async (network: string) => { + setLoading(true); + try { + const plans = await apiClient.getDataPlans(network, false); + setDataPlans(plans); + } catch (error) { + console.error("Failed to fetch data plans:", error); + setErrorMessage("Failed to load data plans. Please try again."); + } finally { + setLoading(false); + } + }, []); + + // Fetch meter types for electricity + const fetchMeterTypes = useCallback(async () => { + setLoading(true); + try { + const { meterTypes: types } = await apiClient.getElectricitySupportedOptions(); + setMeterTypes(types); + } catch (error) { + console.error("Failed to fetch meter types:", error); + } finally { + setLoading(false); + } + }, []); + + // Verify meter number + const handleVerifyMeter = useCallback(async () => { + if (!formData.customer_id || !formData.service_id) { + setMeterVerificationMessage("Please enter meter number and select provider"); + return; + } + + setVerifyingMeter(true); + setMeterVerificationMessage(""); + + try { + const result = await apiClient.verifyElectricityMeter( + formData.service_id, + formData.customer_id + ); + + if (result.success && result.data && result.data.valid) { + setMeterVerified(true); + const customerInfo = result.data.customerName + ? `✓ ${result.data.customerName} ` + : `✓ Meter verified: ${result.data.company}`; + setMeterVerificationMessage(customerInfo); + } else { + setMeterVerified(false); + setMeterVerificationMessage(result.message || "✗ Invalid meter number"); + } + } catch (error: any) { + setMeterVerified(false); + setMeterVerificationMessage( + error.message || "Verification failed. Please try again." + ); + } finally { + setVerifyingMeter(false); + } + }, [formData.customer_id, formData.service_id]); + + // Get expected crypto amount + const fetchExpectedAmount = useCallback(async () => { + try { + let expectedAmount: ExpectedAmount; + + if (type === "airtime") { + const amount = parseFloat(formData.amount); + expectedAmount = await apiClient.getAirtimeExpectedAmount( + amount, + selectedToken + ); + } else if (type === "electricity") { + const amount = parseFloat(formData.amount); + expectedAmount = await apiClient.getElectricityExpectedAmount( + amount, + selectedToken + ); + } else if (type === "data" && formData.dataplan) { + expectedAmount = await apiClient.getDataExpectedAmount( + formData.dataplan.dataplanId, + formData.service_id, + selectedToken + ); + } else { + throw new Error("Invalid purchase configuration"); + } + + setFormData((prev) => ({ ...prev, expectedAmount })); + } catch (error: any) { + console.error("Failed to fetch expected amount:", error); + setErrorMessage(error.message || "Failed to calculate crypto amount"); + } + }, [type, formData.amount, formData.dataplan, formData.service_id, selectedToken]); + + // Current wallet balance for selected token + const currentWalletBalance = useMemo(() => { + const balanceInfo = balances.find( + (b) => (b.chain || "").toLowerCase() === selectedToken.toLowerCase() + ); + return parseFloat(balanceInfo?.balance || "0"); + }, [balances, selectedToken]); + + // Current wallet address for selected token + const currentWalletAddress = useMemo(() => { + if (!addresses) return ""; + const addressInfo = addresses.find( + (addr) => (addr.chain || "").toLowerCase() === selectedToken.toLowerCase() + ); + return addressInfo?.address || ""; + }, [addresses, selectedToken]); + + // Current network for selected token + const currentNetwork = useMemo(() => { + if (!addresses) return "testnet"; + const addressInfo = addresses.find( + (addr) => (addr.chain || "").toLowerCase() === selectedToken.toLowerCase() + ); + return addressInfo?.network || "testnet"; + }, [addresses, selectedToken]); + + // Required crypto amount + const requiredCryptoAmount = useMemo(() => { + if (!formData.expectedAmount?.cryptoAmount) return 0; + const amount = formData.expectedAmount.cryptoAmount; + return Math.ceil(amount * 1e7) / 1e7; + }, [formData.expectedAmount]); + + // Estimate crypto from exchange rates fallback + const estimateCryptoFromRates = useCallback( + (ngnAmount: number, token: string) => { + try { + const r: any = rates || {}; + const rateFor = r[token]; + if (!rateFor || !rateFor.ngn) return 0; + const crypto = ngnAmount / parseFloat(String(rateFor.ngn)); + return Math.ceil(crypto * 1e7) / 1e7; + } catch (e) { + return 0; + } + }, + [rates] + ); + + // Find token with sufficient balance + const findTokenWithSufficientBalance = useCallback( + (ngnAmount: number) => { + const rateMap: any = rates || {}; + const candidates = (balances || []) + .map((b: any) => { + const token = b.chain; + const bal = parseFloat(b.balance || "0"); + const rate = rateMap[token]?.ngn ? parseFloat(rateMap[token].ngn) : 0; + return { token, bal, rate, ngnValue: bal * (rate || 0) }; + }) + .sort((a: any, b: any) => b.ngnValue - a.ngnValue); + + const curr = candidates.find((c: any) => c.token === selectedToken); + if (curr && curr.ngnValue >= ngnAmount) return curr.token; + + const found = candidates.find((c: any) => c.ngnValue >= ngnAmount); + return found ? found.token : null; + }, + [balances, rates, selectedToken] + ); + + // Validation error + const validationError = useMemo(() => { + const merchantAddress = getToAddress(selectedToken.toLowerCase()) || ""; + + if (!currentWalletAddress) { + return "No wallet found for this currency. Add a wallet or select another currency."; + } + + if (!merchantAddress && process.env.NODE_ENV === "production") { + return `Merchant wallet for ${selectedToken.toUpperCase()} is not configured.`; + } + + if (!toAddress.trim()) { + return "Recipient address is required"; + } + + if (!formData.amount || parseFloat(formData.amount) <= 0) { + return "Amount must be greater than 0"; + } + + if (requiredCryptoAmount > currentWalletBalance) { + return "Insufficient balance"; + } + + if (type === "electricity" && config.showVerifyButton && !meterVerified) { + return "Please verify meter number first"; + } + + return null; + }, [ + currentWalletAddress, + selectedToken, + toAddress, + formData.amount, + requiredCryptoAmount, + currentWalletBalance, + type, + config.showVerifyButton, + meterVerified, + getToAddress, + ]); + + // Handle token selection + const handleTokenSelect = useCallback((chain: string) => { + setSelectedToken(chain); + const addr = getToAddress(chain.toLowerCase()); + if (addr) { + setToAddress(addr); + } + }, [getToAddress]); + + // Handle send transaction with PIN + const handleSendWithPin = useCallback(async (pin: string) => { + console.log("phone number", validatePhoneNumber(formData.customer_id),) + + setErrorMessage(""); + + if (validationError) { + setErrorMessage(validationError); + setShowPinDialog(false); + return; + } + + setIsSending(true); + + try { + let normalizedToAddress = toAddress.trim(); + + if (selectedToken === "starknet") { + try { + normalizedToAddress = normalizeStarknetAddress(toAddress, "starknet"); + } catch (error) { + throw new Error( + error instanceof Error + ? `Invalid Starknet address: ${error.message}` + : "Invalid Starknet address format" + ); + } + } + + // Dev mode simulation for self-transfers + // if (normalizedToAddress === currentWalletAddress) { + // if (process.env.NODE_ENV === "production") { + // throw new Error("Recipient address cannot be the same as the sender."); + // } + + // } + + const transactionResponse = await sendTransaction({ + chain: selectedToken, + network: currentNetwork, + toAddress: normalizedToAddress, + amount: requiredCryptoAmount.toString(), + fromAddress: currentWalletAddress, + transactionPin: pin, + }); + + setTxHash(transactionResponse.txHash); + setShowPinDialog(false); + + await handleSubmitPurchase(transactionResponse.txHash); + setStep(4); + } catch (error: any) { + console.error("Transaction error:", error); + let errMsg = "Failed to send transaction. Please try again."; + + if (error.message) { + errMsg = error.message; + } else if (typeof error === "string") { + errMsg = error; + } + + setErrorMessage(errMsg); + setShowPinDialog(false); + } finally { + setIsSending(false); + } + }, [ + validationError, + toAddress, + selectedToken, + currentWalletAddress, + currentNetwork, + requiredCryptoAmount, + sendTransaction, + ]); + + // Submit purchase to backend + const handleSubmitPurchase = useCallback(async (transactionHash: string) => { + setLoading(true); + + try { + let response; + + if (type === "airtime") { + const provider = providers.find((p) => p.serviceID === formData.service_id) || null; + const metadata = { + provider, + expectedAmount: formData.expectedAmount || null, + selectedToken, + fromAddress: currentWalletAddress, + merchantAddress: getToAddress(selectedToken), + purchaseType: "AirtimePurchase", + }; + + response = await apiClient.purchaseAirtime({ + type: "airtime", + amount: parseFloat(formData.amount), + chain: selectedToken, + phoneNumber: validatePhoneNumber(formData.customer_id), + mobileNetwork: formData.service_id, + transactionHash, + } as any); + } else if (type === "data" && formData.dataplan) { + const provider = providers.find((p) => p.serviceID === formData.service_id) || null; + // const metadata = { + // provider, + // dataplan: formData.dataplan, + // expectedAmount: formData.expectedAmount || null, + // selectedToken, + // fromAddress: currentWalletAddress, + // merchantAddress: getToAddress(selectedToken), + // purchaseType: "DataPurchase", + // }; + + response = await apiClient.purchaseData({ + type: "data", + dataplanId: formData.dataplan.dataplanId, + amount: parseFloat(formData.dataplan.amount.replace(/[N₦,]/g, "")), + chain: selectedToken, + phoneNumber: validatePhoneNumber(formData.customer_id), + mobileNetwork: formData.service_id, + transactionHash, + } as any); + } else if (type === "electricity") { + const companyInfo = electricityCompanies.find((c) => c.value === formData.service_id) || null; + const metadata = { + company: companyInfo, + expectedAmount: formData.expectedAmount || null, + selectedToken, + fromAddress: currentWalletAddress, + merchantAddress: getToAddress(selectedToken), + purchaseType: "ElectricityPurchase", + }; + + response = await apiClient.purchaseElectricity({ + type: "electricity", + amount: parseFloat(formData.amount), + chain: selectedToken, + company: formData.service_id, + meterType: formData.meterType, + meterNumber: formData.customer_id, + phoneNumber: validatePhoneNumber(formData.phoneNo), + transactionHash, + } as any); + } else { + throw new Error("Invalid purchase type"); + } + + if (response.success) { + setSuccess(true); + setTransactionData(response.data); + } else { + setSuccess(false); + setErrorMessage(response.message || "Purchase failed"); + } + } catch (error: any) { + console.error("Purchase error:", error); + setSuccess(false); + setErrorMessage(error.message || "Failed to complete purchase"); + } finally { + setLoading(false); + } + }, [ + type, + providers, + electricityCompanies, + formData, + selectedToken, + currentWalletAddress, + getToAddress, + ]); + + // Step 1 validation + const isStep1Valid = useCallback(() => { + if (!formData.service_id) return false; + if (!formData.customer_id) return false; + + if (type === "data" && !formData.dataplan) return false; + if ((type === "airtime" || type === "electricity") && !formData.amount) + return false; + if ( + type === "electricity" && + config.showVerifyButton && + !meterVerified && + !formData.phoneNo + ) + return false; + + return true; + }, [formData, type, meterVerified, config]); + + // Handlers + const handleBack = useCallback(() => { + if (step === 1) { + window.history.back(); + } else { + setStep((prev) => prev - 1); + setErrorMessage(""); + } + }, [step]); + + const handleNext = useCallback(() => { + setErrorMessage(""); + setStep((prev) => prev + 1); + }, []); + + const handleConfirm = useCallback(() => { + if (validationError) { + setErrorMessage(validationError); + return; + } + setShowPinDialog(true); + }, [validationError]); + + const resetForm = useCallback(() => { + setFormData({ + service_id: "", + amount: "", + customer_id: "", + meterType: "prepaid", + dataplan: null, + expectedAmount: null, + transactionData: null, + phoneNo: "", + }); + setToAddress(""); + setTxHash(""); + setErrorMessage(""); + setMeterVerified(false); + setMeterVerificationMessage(""); + setSuccess(null); + setStep(1); + }, []); + + // Effects + useEffect(() => { + fetchProviders(); + if (type === "electricity") { + fetchMeterTypes(); + } + }, [type, fetchProviders, fetchMeterTypes]); + + useEffect(() => { + if (type === "data" && formData.service_id) { + fetchDataPlans(formData.service_id); + } + }, [type, formData.service_id, fetchDataPlans]); + + useEffect(() => { + if (step === 2 && selectedToken) { + fetchExpectedAmount(); + } + }, [step, selectedToken, fetchExpectedAmount]); + + useEffect(() => { + // Auto-fill recipient address + try { + const addr = getToAddress(selectedToken.toLowerCase()); + if (addr && !toAddress) { + setToAddress(addr); + } + } catch (e) { + // ignore + } + }, [selectedToken, addresses, toAddress, getToAddress]); + + useEffect(() => { + // Dev-only fallback + if (process.env.NODE_ENV === "production") return; + try { + const addr = getToAddress(selectedToken.toLowerCase()); + if (!addr && !toAddress && currentWalletAddress) { + setToAddress(currentWalletAddress); + setMerchantFallback(true); + } else { + setMerchantFallback(false); + } + } catch (e) { + // ignore + } + }, [selectedToken, addresses, currentWalletAddress, toAddress, getToAddress]); + + useEffect(() => { + // Auto-select token with sufficient balance + if (step !== 2) return; + const ngnAmount = parseFloat(formData.amount || "0"); + if (!ngnAmount || ngnAmount <= 0) return; + + const expected = formData.expectedAmount; + if (expected && expected.cryptoAmount && expected.cryptoCurrency) { + const expectedToken = expected.chain || expected.cryptoCurrency?.toLowerCase(); + if (expectedToken && expectedToken !== selectedToken) { + setSelectedToken(expectedToken); + setToAddress(getToAddress(expectedToken)); + } + return; + } + + const candidate = findTokenWithSufficientBalance(ngnAmount); + if (candidate && candidate !== selectedToken) { + setSelectedToken(candidate); + setToAddress(getToAddress(candidate)); + } + }, [step, formData.amount, formData.expectedAmount, findTokenWithSufficientBalance, selectedToken, getToAddress]); + + return { + // State + step, + loading, + success, + showPinDialog, + isSending, + selectedToken, + toAddress, + errorMessage, + verifyingMeter, + meterVerified, + meterVerificationMessage, + formData, + providers, + dataPlans, + electricityCompanies, + meterTypes, + transactionData, + txHash, + merchantFallback, + + // Setters + setStep, + setFormData, + setShowPinDialog, + setErrorMessage, + + // Handlers + handleBack, + handleNext, + handleConfirm, + handleTokenSelect, + handleSendWithPin, + handleVerifyMeter, + resetForm, + + // Validation + isStep1Valid, + validationError, + + // Utilities + config, + presetAmounts, + requiredCryptoAmount, + currentWalletBalance, + currentWalletAddress, + currentNetwork, + + // Data + getToAddress, + }; +} \ No newline at end of file diff --git a/components/hooks/useSilentQuery.ts b/components/hooks/useSilentQuery.ts deleted file mode 100644 index 5c4ddde..0000000 --- a/components/hooks/useSilentQuery.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { useState, useEffect, useCallback, useRef } from "react"; - -interface UseSilentQueryOptions { - ttl?: number; - backgroundRefresh?: boolean; - cacheKey: string; -} - -// Simple in-memory cache (per session) -const sessionCache = new Map< - string, - { data: any; timestamp: number; ttl: number } ->(); - -export function useSilentQuery( - fetchFn: () => Promise, - options: UseSilentQueryOptions -) { - const { ttl = 5 * 60 * 1000, backgroundRefresh = true, cacheKey } = options; - - const [data, setData] = useState(null); - const [error, setError] = useState(null); - const mountedRef = useRef(true); - // Keep a ref to the latest fetchFn so callers can pass inline functions - // without causing this hook to re-create callbacks on every render. - const fetchFnRef = useRef(fetchFn); - useEffect(() => { - fetchFnRef.current = fetchFn; - }, [fetchFn]); - - // Cleanup flag to prevent state update on unmounted component - useEffect(() => { - return () => { - mountedRef.current = false; - }; - }, []); - - /** 🔹 Get cached data safely */ - const getCachedData = useCallback((): T | null => { - const cached = sessionCache.get(cacheKey); - if (!cached) return null; - - const isExpired = Date.now() - cached.timestamp > cached.ttl; - if (isExpired) { - sessionCache.delete(cacheKey); - return null; - } - - return cached.data; - }, [cacheKey]); - - /** 🔹 Store data in cache */ - const setCachedData = useCallback( - (data: T) => { - sessionCache.set(cacheKey, { - data, - timestamp: Date.now(), - ttl, - }); - }, - [cacheKey, ttl] - ); - - /** 🔹 Fetch fresh data */ - const fetchData = useCallback( - async (isBackgroundRefresh = false) => { - try { - // call the latest fetchFn from ref to avoid recreating this callback - const result = await fetchFnRef.current(); - setCachedData(result); - - // Avoid state updates if unmounted - if (!mountedRef.current) return; - - setData(result); - setError(null); - } catch (err) { - if (!mountedRef.current) return; - if (!isBackgroundRefresh) { - setError(err instanceof Error ? err.message : "Failed to fetch data"); - } - } - }, - [setCachedData] - ); - - /** 🔹 Initialize once on mount */ - useEffect(() => { - const initializeData = async () => { - const cachedData = getCachedData(); - // If the app is initializing auth, defer fetches until auth is ready. - const isInitializing = typeof window !== "undefined" && (window as any).__VELO_AUTH_INITIALIZING; - - if (cachedData) { - setData(cachedData); - - if (backgroundRefresh) { - // If auth is initializing, subscribe to the auth-ready event and - // perform background refresh once initialization completes. Otherwise - // schedule a silent refresh after 2s. - if (isInitializing && typeof window !== "undefined") { - const onReady = () => { - try { - fetchData(true); - } catch (e) { - /* ignore */ - } - window.removeEventListener("velo:authReady", onReady); - }; - window.addEventListener("velo:authReady", onReady); - } else { - setTimeout(() => { - fetchData(true); - }, 2000); - } - } - } else { - // No cached data: start background fetch only if auth init is finished. - if (isInitializing) { - // wait for auth to finish, then fetch - if (typeof window !== "undefined") { - const onReady = () => { - try { - void fetchData(true); - } catch (e) { - /* ignore */ - } - window.removeEventListener("velo:authReady", onReady); - }; - window.addEventListener("velo:authReady", onReady); - } - } else { - // Start background fetch but don't await it so the UI isn't blocked - // on first mount when no cache exists. Components should provide a - // sessionStorage fallback or loading UI for the very first render. - void fetchData(true); - } - } - }; - - initializeData(); - // ✅ Only depend on stable inputs - }, [cacheKey, backgroundRefresh, getCachedData, fetchData]); - - /** 🔹 Background refresh every 20s (if enabled) */ - useEffect(() => { - if (!backgroundRefresh) return; - - const interval = setInterval(() => { - if (!document.hidden) { - fetchData(true); - } - }, 20000); - - return () => clearInterval(interval); - }, [fetchData, backgroundRefresh]); - - /** 🔹 Manual refetch */ - const refetch = useCallback(async () => { - await fetchData(false); - }, [fetchData]); - - return { data, error, refetch }; -} diff --git a/components/hooks/useTokenBalance.ts b/components/hooks/useTokenBalance.ts index d4738db..63ea4fa 100644 --- a/components/hooks/useTokenBalance.ts +++ b/components/hooks/useTokenBalance.ts @@ -1,216 +1,298 @@ -import { useCallback, useMemo } from "react"; -import { useWalletData } from "./useWalletData"; +import { useMemo, useCallback } from 'react'; +import { useApiQuery } from './useApiQuery'; +import useExchangeRates from '@/components/hooks/useExchangeRate'; +import { + normalizeChain, + getTokenSymbol, + getTokenName, + getRateKey, + formatTokenAmount +} from '@/lib/utils/token-utils'; +import { apiClient } from '@/lib/api-client'; + +export interface TokenInfo { + chain: string; + name: string; + symbol: string; + address: string; + network: string; + balance: number; + ngnValue: number; + hasWallet: boolean; + rate: number; +} export function useTokenBalance() { - const { - addresses, - balances, - breakdown, - - } = useWalletData(); - - const getTokenSymbol = useCallback((chain: string): string => { - const key = chain ? chain.toLowerCase() : chain; - const symbolMap: { [key: string]: string } = { - ethereum: "ETH", - bitcoin: "BTC", - solana: "SOL", - starknet: "STRK", - usdt_erc20: "USDT", - usdt_trc20: "USDT", - polkadot: "DOT", - stellar: "XLM", - }; - return symbolMap[key] || (chain ? chain.toUpperCase() : ""); - }, []); - - // Normalize a provided chain or symbol into a canonical chain key used in the app - const normalizeChain = useCallback((raw: string | undefined | null) => { - if (!raw) return ""; - const k = String(raw).toLowerCase().trim(); - if (k === "sol" || k === "solana" || k.startsWith("sol")) return "solana"; - if (k === "eth" || k === "ethereum") return "ethereum"; - if (k === "btc" || k === "bitcoin") return "bitcoin"; - if (k === "strk" || k === "starknet") return "starknet"; - if (k === "usdt" || k === "usdt_erc20" || k === "usdt-erc20") return "usdt_erc20"; - if (k === "usdt_trc20" || k === "usdt-trc20" || k === "usdttrc20") return "usdt_trc20"; - if (k === "dot" || k === "polkadot") return "polkadot"; - if (k === "xlm" || k === "stellar") return "stellar"; - return k; - }, []); - - const getTokenName = useCallback((chain: string): string => { - const key = chain ? chain.toLowerCase() : chain; - const nameMap: { [key: string]: string } = { - ethereum: "Ethereum", - bitcoin: "Bitcoin", - solana: "Solana", - starknet: "Starknet", - usdt_erc20: "USDT ERC20", - usdt_trc20: "USDT TRC20", - polkadot: "Polkadot", - stellar: "Stellar", - }; - return nameMap[key] || (chain ? chain.charAt(0).toUpperCase() + chain.slice(1) : ""); - }, []); - - // CHANGED: Get balance from balances array instead of breakdown - const getWalletBalance = useCallback( - (chain: string): number => { - if (!balances || !Array.isArray(balances) || balances.length === 0) return 0; - const key = normalizeChain(chain); - const balanceInfo = balances.find( - (b) => normalizeChain(b.chain) === key || normalizeChain(b.symbol) === key - ); - return parseFloat(balanceInfo?.balance || "0"); - }, - [balances, normalizeChain] + // Fetch addresses using useApiQuery (handles caching, loading, errors automatically) + const { + data: addressesData, + isLoading: addressesLoading, + error: addressesError, + refetch: refetchAddresses + } = useApiQuery( + () => apiClient.getWalletAddresses(), + { cacheKey: 'wallet-addresses', ttl: 10 * 60 * 1000 } ); + + // Fetch balances using useApiQuery + const { + data: balancesData, + isLoading: balancesLoading, + error: balancesError, + refetch: refetchBalances + } = useApiQuery( + () => apiClient.getWalletBalances(), + { cacheKey: 'wallet-balances', ttl: 2 * 60 * 1000 } + ); + + // Get exchange rates + const { rates } = useExchangeRates(); + + // Normalize data - safe defaults + const addresses = addressesData || []; + const balances = balancesData || []; - const getWalletAddress = useCallback( - (chain: string): string => { - if (!addresses || !Array.isArray(addresses)) return ""; - const key = normalizeChain(chain); - const addressInfo = addresses.find((addr) => normalizeChain(addr.chain) === key); - return addressInfo?.address || ""; - }, - [addresses, normalizeChain] + // Get token rate from exchange rates + const getTokenRate = useCallback((symbol: string): number => { + const rateKey = getRateKey(symbol); + const rate = rates[rateKey as keyof typeof rates]; + return typeof rate === 'number' ? rate : 1; + }, [rates]); + + // MAIN COMPUTATION: Build unified token info + const availableTokens = useMemo((): TokenInfo[] => { + if (!addresses.length && !balances.length) return []; + + const tokenMap = new Map(); + + // Process addresses + addresses.forEach(addr => { + const chainKey = normalizeChain(addr.chain); + if (!chainKey) return; + + const symbol = getTokenSymbol(chainKey); + const rate = getTokenRate(symbol); + + tokenMap.set(chainKey, { + chain: chainKey, + name: getTokenName(chainKey), + symbol, + address: addr.address, + network: addr.network || 'testnet', + balance: 0, // Will be updated from balances + ngnValue: 0, + hasWallet: true, + rate, + }); + }); + + // Process balances and merge + balances.forEach(bal => { + const chainKey = normalizeChain(bal.chain) || normalizeChain(bal.symbol); + if (!chainKey) return; + + const symbol = bal.symbol || getTokenSymbol(chainKey); + const balance = parseFloat(bal.balance || '0'); + const rate = getTokenRate(symbol); + const ngnValue = balance * rate; + + const existing = tokenMap.get(chainKey); + + if (existing) { + // Update existing token + existing.balance = balance; + existing.ngnValue = ngnValue; + existing.rate = rate; + // If balance record has address but addresses array doesn't + if (!existing.address && bal.address) { + existing.address = bal.address; + existing.hasWallet = true; + } + } else { + // Create new token entry + tokenMap.set(chainKey, { + chain: chainKey, + name: getTokenName(chainKey), + symbol, + address: bal.address || '', + network: bal.network || 'testnet', + balance, + ngnValue, + hasWallet: !!bal.address, + rate, + }); + } + }); + + // Convert to array, sort by value (highest first) + return Array.from(tokenMap.values()) + .sort((a, b) => b.ngnValue - a.ngnValue); + }, [addresses, balances, getTokenRate]); + + // DERIVED DATA - all memoized + const walletTokens = useMemo(() => + availableTokens.filter(token => token.hasWallet), + [availableTokens] ); - const getWalletNetwork = useCallback( - (chain: string): string => { - if (!addresses || !Array.isArray(addresses)) return "testnet"; - const key = normalizeChain(chain); - const addressInfo = addresses.find((addr) => normalizeChain(addr.chain) === key); - return addressInfo?.network || "testnet"; - }, - [addresses, normalizeChain] + const tokensWithBalance = useMemo(() => + availableTokens.filter(token => token.balance > 0), + [availableTokens] ); - const hasWalletForToken = useCallback( - (chain: string): boolean => { - return !!getWalletAddress(chain); - }, - [getWalletAddress] + const totalPortfolioValue = useMemo(() => + availableTokens.reduce((sum, token) => sum + token.ngnValue, 0), + [availableTokens] ); - // FIXED: Build availableTokens from both addresses and balances so chains - // that exist only in balances (e.g., Solana) are included in the dropdown. - const availableTokens = useMemo(() => { - // Also ensure balances and breakdown are arrays - const safeBalances = Array.isArray(balances) ? balances : []; - const safeBreakdown = Array.isArray(breakdown) ? breakdown : []; - - // tokenMap keyed by lowercase chain - const tokenMap: Record = {}; - - if (Array.isArray(addresses)) { - for (const addr of addresses) { - const chainKey = normalizeChain(addr.chain); - if (!chainKey || chainKey === "" || chainKey === "unknown") continue; - tokenMap[chainKey] = tokenMap[chainKey] || {}; - tokenMap[chainKey].chain = chainKey; - tokenMap[chainKey].address = addr.address; - tokenMap[chainKey].network = addr.network; - tokenMap[chainKey].hasWallet = true; - } - } + const primaryToken = useMemo(() => + availableTokens.length > 0 ? availableTokens[0] : null, + [availableTokens] + ); - for (const b of safeBalances) { - // Some backends return a non-standard chain or leave chain empty and only provide symbol. - // Prefer a normalized chain derived from b.chain, but fall back to the symbol when needed. - let chainKey = normalizeChain(b.chain); - if (!chainKey || chainKey === "" || chainKey === "unknown") { - const sym = (b.symbol || "").toLowerCase().trim(); - if (sym === "eth" || sym === "ethereum") chainKey = "ethereum"; - else if (sym === "btc" || sym === "bitcoin") chainKey = "bitcoin"; - else if (sym === "sol" || sym === "solana") chainKey = "solana"; - else if (sym === "strk" || sym === "starknet") chainKey = "starknet"; - else if (sym === "usdt") chainKey = "usdt_erc20"; - } - if (!chainKey || chainKey === "" || chainKey === "unknown") { - // Skip entries we can't normalize to a known chain - continue; - } + - tokenMap[chainKey] = tokenMap[chainKey] || {}; - tokenMap[chainKey].chain = chainKey; - tokenMap[chainKey].balance = parseFloat(b.balance || "0") || 0; - tokenMap[chainKey].symbol = tokenMap[chainKey].symbol || (b.symbol || getTokenSymbol(chainKey)); - } + // DEBUG: Test normalization + const testNormalization = (input: string) => { + const normalized = normalizeChain(input); + return normalized; + }; + + // Get token by chain - WITH DEBUG + const getTokenByChain = useCallback((chain: string): TokenInfo | null => { + const key = normalizeChain(chain); + + const found = availableTokens.find(token => token.chain === key) || null; + return found; + }, [availableTokens]); - // Attach breakdown ngn values if available - for (const br of safeBreakdown) { - const chainKey = normalizeChain(br.chain); - tokenMap[chainKey] = tokenMap[chainKey] || {}; - tokenMap[chainKey].ngnValue = br.ngnValue || 0; + // Get token by symbol - WITH DEBUG + const getTokenBySymbol = useCallback((symbol: string): TokenInfo | null => { + const normalizedSymbol = symbol.toUpperCase(); + + const found = availableTokens.find(token => + token.symbol.toUpperCase() === normalizedSymbol + ) || null; + return found; + }, [availableTokens]); + + // Combined getTokenInfo - WITH DEBUG + const getTokenInfo = useCallback((chainOrSymbol: string): TokenInfo | null => { + const chainKey = normalizeChain(chainOrSymbol); + const byChain = getTokenByChain(chainKey); + if (byChain) { + return byChain; + } + + // Try by symbol + const bySymbol = getTokenBySymbol(chainOrSymbol); + if (bySymbol) { + return bySymbol; } + return null; + }, [getTokenByChain, getTokenBySymbol]); - // Convert map to array and fill in derived fields - const tokens = Object.keys(tokenMap) - .filter((k) => k && k !== "unknown") - .map((chainKey) => { - const entry = tokenMap[chainKey]; - return { - chain: chainKey, - name: getTokenName(chainKey), - symbol: entry.symbol || getTokenSymbol(chainKey), - address: entry.address || "", - network: entry.network || "testnet", - balance: typeof entry.balance === "number" ? entry.balance : 0, - ngnValue: entry.ngnValue || 0, - hasWallet: !!entry.address, - }; - }) - .sort((a, b) => a.chain.localeCompare(b.chain)); - - // Ensure core chains (ethereum, bitcoin, solana) are present when balances contain only symbols - const ensureFromBalances = (symList: Array<{ chain: string; sym: string }>) => { - for (const { chain, sym } of symList) { - const exists = tokens.find((t) => t.chain === chain); - if (!exists) { - const match = safeBalances.find((b) => ((b.symbol || "") + "").toLowerCase() === sym.toLowerCase()); - if (match) { - tokens.push({ - chain, - name: getTokenName(chain), - symbol: match.symbol || getTokenSymbol(chain), - address: match.address || "", - network: match.network || "testnet", - balance: parseFloat(match.balance || "0") || 0, - ngnValue: 0, - hasWallet: !!match.address, - }); - } - } - } - }; + // Add test function + const testLookup = useCallback((input: string) => { + const result = getTokenInfo(input); + return result; + }, [getTokenInfo]); - ensureFromBalances([ - { chain: "ethereum", sym: "eth" }, - { chain: "bitcoin", sym: "btc" }, - { chain: "solana", sym: "sol" }, - ]); + // CORE FUNCTIONS (using the memoized lookups) + const getWalletBalance = useCallback((chain: string): number => + getTokenByChain(chain)?.balance || 0, + [getTokenByChain] + ); - if (typeof window !== "undefined") { - // debug: help devs see why tokens may be missing - console.debug("useTokenBalance: availableTokens", tokens); - } + const getWalletAddress = useCallback((chain: string): string => + getTokenByChain(chain)?.address || '', + [getTokenByChain] + ); + + const getWalletNetwork = useCallback((chain: string): string => + getTokenByChain(chain)?.network || 'testnet', + [getTokenByChain] + ); - return tokens; - }, [addresses, balances, breakdown, getTokenName, getTokenSymbol, normalizeChain]); + const hasWalletForToken = useCallback((chain: string): boolean => + getTokenByChain(chain)?.hasWallet || false, + [getTokenByChain] + ); + + + const isTokenSupported = useCallback((chainOrSymbol: string): boolean => { + const key = normalizeChain(chainOrSymbol); + return availableTokens.some(token => + token.chain === key || + token.symbol.toUpperCase() === chainOrSymbol.toUpperCase() + ); + }, [availableTokens]); + + // FORMATTING HELPERS + const formatBalance = useCallback((chain: string, maxDecimals = 6): string => { + const token = getTokenByChain(chain); + if (!token) return '0'; + return formatTokenAmount(token.balance, chain, { maxDecimals }); + }, [getTokenByChain]); + + const formatValue = useCallback((chain: string, currency: 'NGN' | 'USD' = 'NGN'): string => { + const token = getTokenByChain(chain); + if (!token) return currency === 'NGN' ? '₦0' : '$0'; + + const value = token.ngnValue; + const formatted = new Intl.NumberFormat('en-NG', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(value); + + return currency === 'NGN' ? `₦${formatted}` : `$${(value / 1500).toFixed(2)}`; + }, [getTokenByChain]); + const isLoading = addressesLoading || balancesLoading; + const error = addressesError || balancesError; + + const refetch = useCallback(async () => { + await Promise.all([refetchAddresses(), refetchBalances()]); + }, [refetchAddresses, refetchBalances]); return { - getTokenSymbol, - getTokenName, + availableTokens, + walletTokens, + tokensWithBalance, + testLookup, // Add test function + debugData: { + addresses, + balances, + availableTokens, + exchangeRates: rates + }, + getTokenBySymbol, + getTokenByChain, + getTokenInfo, + isTokenSupported, + getWalletBalance, getWalletAddress, getWalletNetwork, hasWalletForToken, - addresses: Array.isArray(addresses) ? addresses : [], - balances: Array.isArray(balances) ? balances : [], - breakdown: Array.isArray(breakdown) ? breakdown : [], - availableTokens, + + getTokenSymbol, + getTokenName, + getTokenRate: (symbol: string) => getTokenRate(symbol), + + totalPortfolioValue, + primaryToken, + + formatBalance, + formatValue, + + isLoading, + error, + refetch, + + addresses, + balances, + addressesCount: addresses.length, + balancesCount: balances.length, }; } \ No newline at end of file diff --git a/components/hooks/useTokenMonitor.ts b/components/hooks/useTokenMonitor.ts index 1fc36e7..68b1625 100644 --- a/components/hooks/useTokenMonitor.ts +++ b/components/hooks/useTokenMonitor.ts @@ -1,4 +1,3 @@ -// hooks/useTokenMonitor.ts import { useEffect, useState } from 'react'; import { tokenManager } from '../lib/api'; import { signOut } from 'next-auth/react'; diff --git a/components/hooks/useTransactions.ts b/components/hooks/useTransactions.ts index b5dd04e..eb6698f 100644 --- a/components/hooks/useTransactions.ts +++ b/components/hooks/useTransactions.ts @@ -2,7 +2,7 @@ import { useState, useCallback, useMemo, useEffect } from 'react'; import { apiClient } from '@/lib/api-client'; import { useAuth } from '@/components/context/AuthContext'; import { Transaction, TransactionHistoryResponse } from '@/types/authContext'; -import { useSilentQuery } from './useSilentQuery'; +import { useApiQuery } from './useApiQuery'; interface UseTransactionsParams { page?: number; @@ -41,7 +41,7 @@ export const useTransactions = (initialParams: UseTransactionsParams = {}): UseT data: transactionsData, error: transactionsError, refetch: refetchTransactions - } = useSilentQuery( + } = useApiQuery( () => apiClient.getTransactionHistory({ ...params, page: 1 }), { cacheKey: `transactions-${JSON.stringify({ ...params, page: 1 })}`, diff --git a/components/hooks/useWalletData.ts b/components/hooks/useWalletData.ts index 233a6cb..8674544 100644 --- a/components/hooks/useWalletData.ts +++ b/components/hooks/useWalletData.ts @@ -1,221 +1,42 @@ -import { useCallback, useMemo } from 'react'; -import { apiClient } from '@/lib/api-client'; -import { useAuth } from '@/components/context/AuthContext'; -import { WalletAddress, WalletBalance } from '@/types/authContext'; +import { useAuthQuery } from './useAuthQuery'; +import { normalizeChain, getTokenSymbol, getRateKey } from '@/lib/utils/token-utils'; import useExchangeRates from './useExchangeRate'; -import { useSilentQuery } from './useSilentQuery'; - -interface BalanceBreakdown { - chain: string; - symbol: string; - balance: number; - ngnValue: number; - rate: number | null; -} - -interface WalletData { - addresses: WalletAddress[]; - balances: WalletBalance[]; - breakdown: BalanceBreakdown[]; - totalNGN: number; - totalBalance: number; - error: string | null; - refetch: () => Promise; - isLoading: boolean; -} - -export const useWalletData = (): WalletData => { - const { token } = useAuth(); - const { rates, isLoading: ratesLoading } = useExchangeRates(); - - +import { apiClient } from '@/lib/api-client'; - // Use silent queries - const { - data: addressesData, - error: addressesError, - refetch: refetchAddresses - } = useSilentQuery( - async () => { - try { - const result = await apiClient.getWalletAddresses(); - const wallets = result.filter(address => address.chain !== "usdt_trc20") - // console.log("filleterd wallets data",wallets) - return wallets; - } catch (error) { - throw error; - } - }, - { - cacheKey: 'wallet-addresses', - ttl: 10 * 60 * 1000, - backgroundRefresh: true - } +export const useWalletData = () => { + const { data: addressesData, ...addressesRest } = useAuthQuery( + () => apiClient.getWalletAddresses(), + { cacheKey: 'wallet-addresses', ttl: 10 * 60 * 1000 } ); - const { - data: balancesData, - error: balancesError, - refetch: refetchBalances - } = useSilentQuery( - async () => { - try { - const result = await apiClient.getWalletBalances(); - const wallets = result.filter(address => address.chain !== "usdt_trc20") - // console.log("fileterd balance", wallets) - return wallets; - } catch (error) { - throw error; - } - }, - { - cacheKey: 'wallet-balances', - ttl: 2 * 60 * 1000, - backgroundRefresh: true - } + const { data: balancesData, ...balancesRest } = useAuthQuery( + () => apiClient.getWalletBalances(), + { cacheKey: 'wallet-balances', ttl: 2 * 60 * 1000 } ); - // Fast-path: read a lightweight cached copy from sessionStorage so the UI - // can render balances immediately on cold reload while network fetch runs. - // Keys are namespaced to avoid collision and are optional. - const readFallback = (key: string): T | null => { - if (typeof window === 'undefined') return null; - try { - const raw = sessionStorage.getItem(key); - if (!raw) return null; - return JSON.parse(raw) as T; - } catch (e) { - return null; - } - }; - - const fallbackAddresses = readFallback('velo.wallet.addresses'); - const fallbackBalances = readFallback('velo.wallet.balances'); - - - // SAFE data processing - const addresses = useMemo(() => { - let result: WalletAddress[] = []; - // Prefer live query data, but fall back to sessionStorage cache so the UI - // can show something instantly while the background fetch completes. - if (Array.isArray(addressesData)) { - result = addressesData; - } else if (addressesData && typeof addressesData === 'object') { - result = (addressesData as any).addresses || []; - } else if (Array.isArray(fallbackAddresses)) { - result = fallbackAddresses; - } - - return result; - }, [addressesData]); - - const balances = useMemo(() => { - let result: WalletBalance[] = []; - // Prefer live query balances, then fallback to session cache if available. - if (Array.isArray(balancesData)) { - result = balancesData; - } else if (balancesData && typeof balancesData === 'object') { - result = (balancesData as any).balances || []; - } else if (Array.isArray(fallbackBalances)) { - result = fallbackBalances; - } - - return result; - }, [balancesData]); - - // Debug: expose raw and normalized wallet data to the console to help diagnose - // missing tokens in the dropdown (temporary; remove after debugging). - if (typeof window !== 'undefined') { - console.debug('useWalletData: raw', { addressesData, balancesData }); - console.debug('useWalletData: normalized', { addresses, balances }); - } - - const error = addressesError || balancesError; - - // Map symbols to rate keys - const getRateKey = useCallback((symbol: string): keyof typeof rates => { - const symbolMap: { [key: string]: keyof typeof rates } = { - 'ETH': 'ETH', 'BTC': 'BTC', 'SOL': 'SOL', 'STRK': 'STRK', - 'USDT': 'USDT', 'USDC': 'USDC', 'DOT': 'DOT', 'XLM': 'XML' + const { rates } = useExchangeRates(); + + // Simplified processing - logic moved to utils + const addresses = addressesData || []; + const balances = balancesData || []; + + // Calculate breakdown using shared utilities + const breakdown = balances.map(balance => { + const rateKey = getRateKey(balance.symbol || 'USDT'); + const rate = rates[rateKey] || 1; + const numericBalance = parseFloat(balance.balance || "0") || 0; + const ngnValue = numericBalance * rate; + + return { + chain: balance.chain || 'unknown', + symbol: balance.symbol || 'UNKNOWN', + balance: numericBalance, + ngnValue, + rate }; - return symbolMap[symbol] || 'USDT'; - }, []); - - // Calculate breakdown - const { breakdown, totalNGN } = useMemo(() => { - - if (!Array.isArray(balances) || balances.length === 0) { - return { breakdown: [], totalNGN: 0 }; - } - - try { - const breakdownResult = balances.map(balance => { - const rateKey = getRateKey(balance.symbol || 'USDT'); - const rate = rates[rateKey] || 1; - const numericBalance = parseFloat(balance.balance || "0") || 0; - const ngnValue = numericBalance * rate; + }); - return { - chain: balance.chain || 'unknown', - symbol: balance.symbol || 'UNKNOWN', - balance: numericBalance, - ngnValue, - rate - }; - }); - - const totalNGN = breakdownResult.reduce((sum, item) => sum + item.ngnValue, 0); - - return { - breakdown: breakdownResult, - totalNGN - }; - } catch (err) { - console.error('Breakdown calculation error:', err); - return { breakdown: [], totalNGN: 0 }; - } - }, [balances, rates, getRateKey]); - - // Manual refetch - const refetch = useCallback(async () => { - if (!token) { - console.warn(' No token for refetch'); - return; - } - - try { - await Promise.all([refetchAddresses(), refetchBalances()]); - // Persist fresh results (if available) to sessionStorage for instant - // render on next load. We read the live query variables which will be - // updated by the refetch calls above. - try { - if (typeof window !== 'undefined') { - if (addressesData) sessionStorage.setItem('velo.wallet.addresses', JSON.stringify(addressesData)); - if (balancesData) sessionStorage.setItem('velo.wallet.balances', JSON.stringify(balancesData)); - } - } catch (e) { - // ignore storage failures - } - } catch (err) { - console.error(' Manual refetch failed:', err); - } - }, [token, refetchAddresses, refetchBalances]); - - // Persist any fresh query data to sessionStorage as soon as it's available so - // subsequent navigations/read reloads display instantly. - if (typeof window !== 'undefined') { - try { - if (addressesData) sessionStorage.setItem('velo.wallet.addresses', JSON.stringify(addressesData)); - if (balancesData) sessionStorage.setItem('velo.wallet.balances', JSON.stringify(balancesData)); - } catch (e) { - // ignore quota/storage issues - } - } - - // Temporary loading state for debugging - const isLoading = !addressesData && !balancesData && !error; - - // console.log("breakdown ", breakdown) + const totalNGN = breakdown.reduce((sum, item) => sum + item.ngnValue, 0); return { addresses, @@ -223,8 +44,10 @@ export const useWalletData = (): WalletData => { breakdown, totalNGN, totalBalance: totalNGN, - error, - refetch, - isLoading + error: addressesRest.error || balancesRest.error, + refetch: async () => { + await Promise.all([addressesRest.refetch(), balancesRest.refetch()]); + }, + isLoading: addressesRest.isLoading || balancesRest.isLoading, }; }; \ No newline at end of file diff --git a/components/modals/TransactionStatus.tsx b/components/modals/TransactionStatus.tsx new file mode 100644 index 0000000..a009db7 --- /dev/null +++ b/components/modals/TransactionStatus.tsx @@ -0,0 +1,514 @@ +// /components/transactions/TransactionStatus.tsx +import React from 'react'; +import { + Check, + TriangleAlert, + AlertCircle, + ArrowUpRight, + X, + Loader2, + ExternalLink, + Info, + ShieldAlert +} from "lucide-react"; +import { Button } from "@/components/ui/buttons"; +import { Card } from "@/components/ui/Card"; +import { getBlockExplorerUrl } from "@/lib/utils/qr-utils"; + +export type StatusType = "success" | "error" | "warning" | "info" | "loading" | "pending"; + +export interface TransactionStatusProps { + type: StatusType; + message: string; + txHash?: string; + network?: string; + chain?: string; + onDismiss?: () => void; + showIcon?: boolean; + showAction?: boolean; + actionText?: string; + onAction?: () => void; + className?: string; + showExplorerLink?: boolean; + autoDismiss?: boolean; + dismissAfter?: number; +} + +export interface ValidationErrorProps { + error: string | React.ReactNode; + title?: string; + showIcon?: boolean; + variant?: 'default' | 'warning' | 'danger'; + className?: string; + onRetry?: () => void; + retryText?: string; +} + +export interface SuccessMessageProps { + message: string; + txHash?: string; + explorerUrl?: string; + chain?: string; + network?: string; + title?: string; + showIcon?: boolean; + showCopyButton?: boolean; + onClose?: () => void; + className?: string; +} + +/** + * Unified Transaction Status Component + * Supports multiple status types with consistent styling + */ +export const TransactionStatus: React.FC = ({ + type, + message, + txHash, + network = "mainnet", + chain = "ethereum", + onDismiss, + showIcon = true, + showAction = false, + actionText = "View Details", + onAction, + className = "", + showExplorerLink = true, + autoDismiss = false, + dismissAfter = 5000, +}) => { + const [isVisible, setIsVisible] = React.useState(true); + + // Auto-dismiss for success messages + React.useEffect(() => { + if (autoDismiss && type === "success" && isVisible) { + const timer = setTimeout(() => { + setIsVisible(false); + onDismiss?.(); + }, dismissAfter); + return () => clearTimeout(timer); + } + }, [autoDismiss, type, dismissAfter, isVisible, onDismiss]); + + const getStatusConfig = () => { + const configs: Record = { + success: { + bg: "bg-green-500/10", + border: "border-green-500/20", + icon: , + text: "text-green-500", + iconColor: "text-green-500", + }, + error: { + bg: "bg-red-500/10", + border: "border-red-500/20", + icon: , + text: "text-red-500", + iconColor: "text-red-500", + }, + warning: { + bg: "bg-yellow-500/10", + border: "border-yellow-500/20", + icon: , + text: "text-yellow-500", + iconColor: "text-yellow-500", + }, + info: { + bg: "bg-blue-500/10", + border: "border-blue-500/20", + icon: , + text: "text-blue-500", + iconColor: "text-blue-500", + }, + loading: { + bg: "bg-gray-500/10", + border: "border-gray-500/20", + icon: , + text: "text-gray-500", + iconColor: "text-gray-500", + }, + pending: { + bg: "bg-purple-500/10", + border: "border-purple-500/20", + icon: , + text: "text-purple-500", + iconColor: "text-purple-500", + }, + }; + return configs[type]; + }; + + if (!isVisible) return null; + + const config = getStatusConfig(); + const explorerUrl = txHash ? getBlockExplorerUrl(chain, txHash, network) : undefined; + + return ( +
+
+
+ {showIcon && ( +
+ {config.icon} +
+ )} + +
+
+ {type === "loading" ? "Processing..." : type} +
+

+ {message} +

+ + {txHash && showExplorerLink && explorerUrl && explorerUrl !== "#" && ( + + View on Explorer + + + )} +
+
+ +
+ {showAction && onAction && ( + + )} + + {onDismiss && type !== "loading" && ( + + )} +
+
+
+ ); +}; + +/** + * Unified Validation Error Component + * For form validation errors and input warnings + */ +export const ValidationError: React.FC = ({ + error, + title = "Validation Error", + showIcon = true, + variant = 'default', + className = "", + onRetry, + retryText = "Try Again", +}) => { + const getVariantConfig = () => { + const configs = { + default: { + bg: "bg-red-50 dark:bg-red-500/10", + border: "border-red-200 dark:border-red-500/20", + text: "text-red-800 dark:text-red-500", + iconColor: "text-red-500", + }, + warning: { + bg: "bg-yellow-50 dark:bg-yellow-500/10", + border: "border-yellow-200 dark:border-yellow-500/20", + text: "text-yellow-800 dark:text-yellow-500", + iconColor: "text-yellow-500", + }, + danger: { + bg: "bg-red-100 dark:bg-red-500/20", + border: "border-red-300 dark:border-red-500/30", + text: "text-red-900 dark:text-red-400", + iconColor: "text-red-600 dark:text-red-400", + }, + }; + return configs[variant]; + }; + + const config = getVariantConfig(); + + return ( +
+
+ {showIcon && ( + + )} + +
+
+ {title} +
+
+ {typeof error === 'string' ? error : error} +
+ + {onRetry && ( + + )} +
+
+
+ ); +}; + +/** + * Unified Success Message Component + * For transaction success and other success states + */ +export const SuccessMessage: React.FC = ({ + message, + txHash, + explorerUrl, + chain = "ethereum", + network = "mainnet", + title = "Success!", + showIcon = true, + showCopyButton = false, + onClose, + className = "", +}) => { + const [copied, setCopied] = React.useState(false); + + const finalExplorerUrl = explorerUrl || + (txHash ? getBlockExplorerUrl(chain, txHash, network) : undefined); + + const handleCopy = async () => { + if (!txHash) return; + + try { + await navigator.clipboard.writeText(txHash); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error("Failed to copy:", err); + } + }; + + return ( + +
+
+ {showIcon && ( +
+ +
+ )} + +
+

+ {title} +

+

+ {message} +

+ + {txHash && ( +
+
+ + {txHash.slice(0, 16)}...{txHash.slice(-8)} + + + {showCopyButton && ( + + )} +
+ + {finalExplorerUrl && finalExplorerUrl !== "#" && ( + + View transaction on explorer + + + )} +
+ )} +
+
+ + {onClose && ( + + )} +
+
+ ); +}; + +/** + * Loading State Component for Transactions + */ +export const TransactionLoading: React.FC<{ + message?: string; + showSpinner?: boolean; + className?: string; +}> = ({ + message = "Processing transaction...", + showSpinner = true, + className = "", +}) => { + return ( +
+
+ {showSpinner && ( +
+ +
+
+ )} +

{message}

+
+
+ ); +}; + +/** + * Transaction Status Indicator (Small inline version) + */ +export const StatusBadge: React.FC<{ + status: StatusType; + label?: string; + size?: 'sm' | 'md' | 'lg'; + showDot?: boolean; +}> = ({ + status, + label, + size = 'md', + showDot = true +}) => { + const sizeClasses = { + sm: 'text-xs px-2 py-0.5', + md: 'text-sm px-2.5 py-1', + lg: 'text-base px-3 py-1.5', + }; + + const statusConfig = { + success: { + bg: 'bg-green-100 dark:bg-green-500/20', + text: 'text-green-800 dark:text-green-400', + dot: 'bg-green-500', + }, + error: { + bg: 'bg-red-100 dark:bg-red-500/20', + text: 'text-red-800 dark:text-red-400', + dot: 'bg-red-500', + }, + warning: { + bg: 'bg-yellow-100 dark:bg-yellow-500/20', + text: 'text-yellow-800 dark:text-yellow-400', + dot: 'bg-yellow-500', + }, + info: { + bg: 'bg-blue-100 dark:bg-blue-500/20', + text: 'text-blue-800 dark:text-blue-400', + dot: 'bg-blue-500', + }, + loading: { + bg: 'bg-gray-100 dark:bg-gray-500/20', + text: 'text-gray-800 dark:text-gray-400', + dot: 'bg-gray-500 animate-pulse', + }, + pending: { + bg: 'bg-purple-100 dark:bg-purple-500/20', + text: 'text-purple-800 dark:text-purple-400', + dot: 'bg-purple-500', + }, + }[status]; + + return ( + + {showDot && ( + + )} + {label || status.charAt(0).toUpperCase() + status.slice(1)} + + ); +}; + +/** + * Empty Transactions State + */ +export const EmptyTransactions: React.FC<{ + title?: string; + description?: string; + icon?: React.ReactNode; + actionText?: string; + onAction?: () => void; +}> = ({ + title = "No transactions yet", + description = "Your transaction history will appear here", + icon, + actionText, + onAction, +}) => { + return ( +
+
+ {icon || } +
+

{title}

+

{description}

+ {actionText && onAction && ( + + )} +
+ ); +}; + +export default { + TransactionStatus, + ValidationError, + SuccessMessage, + TransactionLoading, + StatusBadge, + EmptyTransactions, +}; \ No newline at end of file diff --git a/components/modals/walletSAndBalance.tsx b/components/modals/walletSAndBalance.tsx new file mode 100644 index 0000000..deccdff --- /dev/null +++ b/components/modals/walletSAndBalance.tsx @@ -0,0 +1,182 @@ +"use client"; + +import React, { memo, useMemo, useCallback } from "react"; +import { useTokenBalance } from "../hooks"; +import chroma from "chroma-js"; +import useExchangeRates from "../hooks/useExchangeRate"; +import { formatToNGN } from "@/lib/utils"; +import { Loader2 } from "lucide-react"; + +const TokenCard = memo(({ + token, + isSelected, + baseColor, + onSelect, + getBalance, + calculateNGN +}: { + token: any; + isSelected: boolean; + baseColor: string; + onSelect: (chain: string) => void; + getBalance: (chain: string) => string; + calculateNGN: (bal: number, chain: string) => number; +}) => { + const textColor = useMemo(() => + chroma.contrast(baseColor, "#FFFFFF") > 4.5 ? "#FFFFFF" : "#000000", + [baseColor] + ); + + const gradient = useMemo(() => { + const darker = chroma(baseColor).darken(0.3).hex(); + const lighter = chroma(baseColor).brighten(0.3).hex(); + return `linear-gradient(135deg, ${darker} 0%, ${baseColor} 50%, ${lighter} 100%)`; + }, [baseColor]); + + const shadow = useMemo(() => { + const shadowColor = chroma(baseColor).darken(0.5).alpha(0.3).css(); + return `0 10px 25px -5px ${shadowColor}, 0 5px 10px 2px ${shadowColor}`; + }, [baseColor]); + + const handleClick = useCallback(() => { + onSelect(token.chain); + }, [onSelect, token.chain]); + + return ( + + ); +}); + +TokenCard.displayName = "TokenCard"; + +function WalletSAndBalance({ + selectedToken, + setSelectedToken, +}: { + selectedToken: string | null; + setSelectedToken: (arg: string) => void; +}) { + const { rates } = useExchangeRates(); + const { + tokensWithBalance, + isLoading, + availableTokens, + balances, + getTokenSymbol, + } = useTokenBalance(); + + console.log("WalletSAndBalance render", { + tokensCount: tokensWithBalance?.length, + selectedToken, + isLoading + }); + + const calculateNGN = useCallback((bal: number | string, chain: string) => { + if (!rates || Object.keys(rates).length === 0) return 0; + const tokenSymbol = getTokenSymbol(chain).toUpperCase(); + const rate = rates[tokenSymbol as keyof typeof rates]; + if (!rate) { + console.warn(`No rate found for ${tokenSymbol} (chain: ${chain})`); + return 0; + } + const balance = typeof bal === "string" ? parseFloat(bal) : bal; + const ngnValue = balance * rate; + return ngnValue; + }, [rates, getTokenSymbol]); + + const tokenColors = useMemo(() => { + const colorMap = new Map(); + if (tokensWithBalance.length > 0) { + tokensWithBalance.forEach((token, index) => { + const hue = (index * (360 / tokensWithBalance.length)) % 360; + const color = chroma.hsl(hue, 0.7, 0.6).hex(); + colorMap.set(token.chain, color); + }); + } + return colorMap; + }, [tokensWithBalance]); + + const getBalance = useCallback((chain: string) => { + const balance = balances?.find((bal) => bal.chain === chain); + return parseFloat(balance?.balance || "0").toFixed(4); + }, [balances]); + + const handleTokenSelect = useCallback((chain: string) => { + console.log("Selecting token:", chain); + setSelectedToken(chain); + }, [setSelectedToken]); + + if (isLoading) { + return ( +
+ {[...Array(4)].map((_, i) => ( +
+ +
+ ))} +
+ ); + } + + if (availableTokens.length === 0) { + return ( +
No tokens available
+ ); + } + + if (tokensWithBalance.length === 0) { + return ( +
+ No tokens with balance available +
+ ); + } + + return ( +
+ {tokensWithBalance.map((token) => ( + + ))} +
+ ); +} + +export default memo(WalletSAndBalance); \ No newline at end of file diff --git a/components/ui/AddressCopyButton.tsx b/components/ui/AddressCopyButton.tsx new file mode 100644 index 0000000..6a69524 --- /dev/null +++ b/components/ui/AddressCopyButton.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; +import { Copy, Check } from "lucide-react"; +import { fixStarknetAddress } from "@/components/lib/utils"; + +interface AddressCopyButtonProps { + address: string; + chain?: string; + size?: 'sm' | 'md' | 'lg'; + showIcon?: boolean; + showText?: boolean; + className?: string; +} + +export const AddressCopyButton: React.FC = ({ + address, + chain = "", + size = 'md', + showIcon = true, + showText = true, + className = "", +}) => { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + if (!address) return; + + try { + const addressToCopy = chain.toLowerCase() === "starknet" + ? fixStarknetAddress(address, chain) + : address; + + await navigator.clipboard.writeText(addressToCopy); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error("Failed to copy address: ", err); + } + }; + + const sizeClasses = { + sm: 'h-6 w-6 text-xs', + md: 'h-8 w-8 text-sm', + lg: 'h-10 w-10 text-base', + }; + + if (!address) return null; + + return ( + + ); +}; \ No newline at end of file diff --git a/components/ui/AddressInput.tsx b/components/ui/AddressInput.tsx new file mode 100644 index 0000000..d0384ec --- /dev/null +++ b/components/ui/AddressInput.tsx @@ -0,0 +1,42 @@ +import React from 'react'; + +interface AddressInputProps { + value: string; + onChange: (value: string) => void; + chain?: string; + placeholder?: string; + disabled?: boolean; + className?: string; +} + +export const AddressInput: React.FC = ({ + value, + onChange, + chain = '', + placeholder = 'Enter wallet address', + disabled = false, + className = '', +}) => { + const getChainLabel = () => { + if (!chain) return ''; + return chain.charAt(0).toUpperCase() + chain.slice(1); + }; + + return ( +
+ onChange(e.target.value)} + placeholder={placeholder} + disabled={disabled} + className="w-full p-3 rounded-lg bg-background border border-border placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors font-mono text-sm disabled:opacity-50" + /> + {chain && ( +
+ {getChainLabel()} +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/components/ui/AmountInput.tsx b/components/ui/AmountInput.tsx new file mode 100644 index 0000000..f116535 --- /dev/null +++ b/components/ui/AmountInput.tsx @@ -0,0 +1,36 @@ +type AmountInputProps = { + value: string; + onChange: (value: string) => void; + currency: string; + disabled?: boolean; + placeholder?: string; +}; + +export const AmountInput = ({ + value, + onChange, + currency, + disabled, + placeholder = "0.00" +}: AmountInputProps) => { + return ( +
+ { + const value = e.target.value; + if (/^\d*\.?\d*$/.test(value)) { + onChange(value); + } + }} + placeholder={placeholder} + className="w-full p-3 rounded-lg border-border bg-muted placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors pr-20 disabled:opacity-50" + disabled={disabled} + /> +
+ {currency} +
+
+ ); +}; \ No newline at end of file diff --git a/components/ui/CardContainer.tsx b/components/ui/CardContainer.tsx new file mode 100644 index 0000000..4af699f --- /dev/null +++ b/components/ui/CardContainer.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Card } from "@/components/ui/Card"; + +interface CardContainerProps { + children: React.ReactNode; + className?: string; + blur?: boolean; + border?: boolean; +} + +export const CardContainer: React.FC = ({ + children, + className = "", + blur = true, + border = true, +}) => { + return ( + + {children} + + ); +}; + +interface InstructionCardProps { + title: string; + items: string[]; + className?: string; +} + +export const InstructionCard: React.FC = ({ + title, + items, + className = "", +}) => { + return ( + +

+ {title} +

+
    + {items.map((item, index) => ( +
  • {item}
  • + ))} +
+
+ ); +}; \ No newline at end of file diff --git a/components/ui/LoadingState.tsx b/components/ui/LoadingState.tsx new file mode 100644 index 0000000..3bda5bf --- /dev/null +++ b/components/ui/LoadingState.tsx @@ -0,0 +1,31 @@ +import { Loader2, Users } from "lucide-react"; +import { Card } from "./cards"; + +export const LoadingState = ({ message = "Loading..." }) => { + return ( +
+
+ +

{message}

+
+
+ ); +}; + +export const EmptyState = ({ + icon: Icon = Users, + title, + description +}: { + icon?: React.ComponentType<{ className?: string }>; + title: string; + description: string; +}) => { + return ( + + +

{title}

+

{description}

+
+ ); +}; \ No newline at end of file diff --git a/components/ui/StepsGuide.tsx b/components/ui/StepsGuide.tsx new file mode 100644 index 0000000..600c7f9 --- /dev/null +++ b/components/ui/StepsGuide.tsx @@ -0,0 +1,31 @@ +import { Card } from "./cards"; + +export const StepsGuide = ({ steps, title = "How It Works" }: {steps: {description: string; title: string}[], title: string}) => { + return ( + +

{title}

+
+ {steps.map((step, index) => ( + + ))} +
+
+ ); +}; + +const StepItem = ({ step, number }: {step:{ + description: string; + title: string; +}, number: number}) => ( +
+
+ {number} +
+
+

{step.title}

+

+ {step.description} +

+
+
+); \ No newline at end of file diff --git a/components/ui/notification.tsx b/components/ui/notification.tsx index 6419792..5eaf98e 100644 --- a/components/ui/notification.tsx +++ b/components/ui/notification.tsx @@ -3,12 +3,11 @@ import { Bell } from "lucide-react"; import { useAuth } from "../context/AuthContext"; import { useNotifications } from "../hooks/useNotifications"; import { apiClient } from "@/lib/api-client"; +import Link from "next/link"; -interface NotificationProps { - onclick: React.Dispatch>; -} -export default function Notification({ onclick }: NotificationProps) { + +export default function Notification() { const [count, setCount] = useState(0); const [isLoading, setIsLoading] = useState(false); const { getUnreadCount } = apiClient; @@ -64,12 +63,11 @@ export default function Notification({ onclick }: NotificationProps) { const handleview = () => { markAllAsRead(); - onclick("Notification"); }; return ( -
)} - + ); } \ No newline at end of file diff --git a/lib/utils.ts b/lib/utils.ts index 6ccd78d..45648d5 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -27,30 +27,68 @@ export const validatePhoneNumber = (num: string): string => { const cleanNum = num.replace(/\D/g, ''); let fixedNum = cleanNum; + let originalFormat = ''; - // Case 1: Numbers starting with 0 -> replace with 234 if (cleanNum.startsWith("0")) { - if (cleanNum.length !== 11) { - throw new Error(`Invalid length: ${num}. Numbers starting with 0 should be 11 digits`); - } - fixedNum = `234${cleanNum.slice(1)}`; + originalFormat = "0-prefix"; + } else if (cleanNum.startsWith("234")) { + originalFormat = "234-prefix"; + } else if (/^[7-9][0-9]{9}$/.test(cleanNum)) { + originalFormat = "10-digit"; } - // Case 2: Numbers starting with 234 -> keep as is - else if (cleanNum.startsWith("234")) { - if (cleanNum.length !== 13) { - throw new Error(`Invalid length: ${num}. Numbers starting with 234 should be 13 digits`); - } + + if (cleanNum.startsWith("0")) { + fixedNum = `234${cleanNum.slice(1)}`; + } else if (cleanNum.startsWith("234")) { fixedNum = cleanNum; - } - // Case 3: Numbers that don't start with 0 or 234 -> throw error - else { - throw new Error(`Invalid format: ${num}. Nigerian numbers must start with 0 or 234`); + } else if (/^[7-9][0-9]{9}$/.test(cleanNum)) { + fixedNum = `234${cleanNum}`; + } else { + if (cleanNum.length === 11 && !cleanNum.startsWith("0")) { + throw new Error(`Invalid format: ${num}. Did you mean 0${cleanNum}?`); + } else if (cleanNum.length === 12 && cleanNum.startsWith("234")) { + throw new Error(`Invalid format: ${num}. Please provide all 13 digits for 234 prefix numbers`); + } else if (cleanNum.length === 9 && /^[7-9][0-9]{8}$/.test(cleanNum)) { + throw new Error(`Invalid format: ${num}. Please provide all 10 digits`); + } else { + throw new Error(`Invalid Nigerian phone number: ${num}. + Valid formats: + • 08101842464 (11 digits starting with 0) + • 8101842464 (10 digits) + • 2348101842464 (13 digits starting with 234)`); + } } - // Final validation for Nigerian number format if (!/^234[7-9][0-9]{9}$/.test(fixedNum)) { + const firstDigitAfter234 = fixedNum.charAt(3); + if (!/[7-9]/.test(firstDigitAfter234)) { + throw new Error(`Invalid Nigerian phone number: ${num}. + Nigerian numbers must start with 070, 080, 081, 090, 091, etc.`); + } + if (fixedNum.length !== 13) { + throw new Error(`Invalid length: ${num}. Expected 13 digits with 234 prefix, got ${fixedNum.length}`); + } throw new Error(`Invalid Nigerian phone number: ${num}`); } return fixedNum; +}; + + +export const formatToNGN = (amount: number = 0, decimal: boolean = true) => { + return new Intl.NumberFormat("en-NG", { + style: "currency", + currency: "NGN", + minimumFractionDigits: !decimal ? 0 : 2, + }).format(amount); +}; + +export const formatToNGNCompact = (amount: number = 0, decimal: boolean = true) => { + return new Intl.NumberFormat("en-NG", { + style: "currency", + currency: "NGN", + notation: "compact", + compactDisplay: "short", + maximumFractionDigits: !decimal ? 0 : 1, + }).format(amount); }; \ No newline at end of file diff --git a/lib/utils/qr-utils.ts b/lib/utils/qr-utils.ts new file mode 100644 index 0000000..7d7ab94 --- /dev/null +++ b/lib/utils/qr-utils.ts @@ -0,0 +1,376 @@ +import QRCodeLib from "qrcode"; + +export interface QRCodeOptions { + amount?: string | null; + label?: string | null; + width?: number; + margin?: number; + darkColor?: string; + lightColor?: string; + errorCorrectionLevel?: "L" | "M" | "Q" | "H"; +} + +export interface QRCodeResult { + dataUrl: string; + rawData: string; + format: string; +} + +export interface ExplorerUrls { + testnet: string; + mainnet: string; +} + +/** + * Generate QR code URI data for different blockchain networks + */ +export const generateQRData = ( + chain: string, + address: string, + amount?: string, + label?: string +): string => { + const normalizedChain = chain.toLowerCase(); + const normalizedAddress = address.trim(); + + switch (normalizedChain) { + case "bitcoin": + case "btc": { + let bitcoinUri = `bitcoin:${normalizedAddress}`; + const bitcoinParams = []; + if (amount) bitcoinParams.push(`amount=${amount}`); + if (label) bitcoinParams.push(`label=${encodeURIComponent(label)}`); + if (bitcoinParams.length > 0) { + bitcoinUri += `?${bitcoinParams.join("&")}`; + } + return bitcoinUri; + } + + case "ethereum": + case "eth": { + let ethereumUri = `ethereum:${normalizedAddress}`; + const ethereumParams = []; + if (amount) { + const weiAmount = (parseFloat(amount) * Math.pow(10, 18)).toString(); + ethereumParams.push(`value=${weiAmount}`); + } + if (label) ethereumParams.push(`label=${encodeURIComponent(label)}`); + if (ethereumParams.length > 0) { + ethereumUri += `?${ethereumParams.join("&")}`; + } + return ethereumUri; + } + + case "solana": + case "sol": { + let solanaUri = `solana:${normalizedAddress}`; + const solanaParams = []; + if (amount) solanaParams.push(`amount=${amount}`); + if (label) solanaParams.push(`label=${encodeURIComponent(label)}`); + if (solanaParams.length > 0) { + solanaUri += `?${solanaParams.join("&")}`; + } + return solanaUri; + } + + case "starknet": + case "strk": { + let starknetUri = `starknet:${normalizedAddress}`; + const starknetParams = []; + if (amount) { + const weiAmount = (parseFloat(amount) * Math.pow(10, 18)).toString(); + starknetParams.push(`value=${weiAmount}`); + } + if (label) starknetParams.push(`label=${encodeURIComponent(label)}`); + if (starknetParams.length > 0) { + starknetUri += `?${starknetParams.join("&")}`; + } + return starknetUri; + } + + case "stellar": + case "xlm": { + let stellarUri = `web+stellar:pay?destination=${normalizedAddress}`; + const stellarParams = []; + if (amount) { + stellarParams.push(`amount=${amount}`); + } + if (label) stellarParams.push(`memo=${encodeURIComponent(label)}`); + if (stellarParams.length > 0) { + stellarUri += `&${stellarParams.join("&")}`; + } + return stellarUri; + } + + case "polkadot": + case "dot": { + let polkadotUri = `substrate:${normalizedAddress}`; + const polkadotParams = []; + if (amount) { + polkadotParams.push(`amount=${amount}`); + } + if (label) polkadotParams.push(`label=${encodeURIComponent(label)}`); + if (polkadotParams.length > 0) { + polkadotUri += `?${polkadotParams.join("&")}`; + } + return polkadotUri; + } + + case "usdt_erc20": + case "usdt_erc": { + return `ethereum:${normalizedAddress}`; + } + + case "usdt_trc20": + case "usdt_trc": { + return `tron:${normalizedAddress}`; + } + + case "erc20": { + let erc20Uri = `ethereum:${normalizedAddress}`; + const erc20Params = []; + if (amount) { + const weiAmount = (parseFloat(amount) * Math.pow(10, 18)).toString(); + erc20Params.push(`value=${weiAmount}`); + } + if (label) erc20Params.push(`label=${encodeURIComponent(label)}`); + if (erc20Params.length > 0) { + erc20Uri += `?${erc20Params.join("&")}`; + } + return erc20Uri; + } + + case "trc20": { + let trc20Uri = `tron:${normalizedAddress}`; + const trc20Params = []; + if (amount) { + const sunAmount = (parseFloat(amount) * Math.pow(10, 6)).toString(); + trc20Params.push(`amount=${sunAmount}`); + } + if (label) trc20Params.push(`label=${encodeURIComponent(label)}`); + if (trc20Params.length > 0) { + trc20Uri += `?${trc20Params.join("&")}`; + } + return trc20Uri; + } + + default: { + // Return plain address for unsupported chains + if (amount || label) { + console.warn(`Chain "${chain}" doesn't support URI schemes with amount/label. Using plain address.`); + } + return normalizedAddress; + } + } +}; + +/** + * Generate a compatible QR code image data URL + */ +export const generateCompatibleQRCode = async ( + chain: string, + address: string, + options: QRCodeOptions = {} +): Promise => { + const { + amount = null, + label = null, + width = 200, + margin = 2, + darkColor = "#000000", + lightColor = "#FFFFFF", + errorCorrectionLevel = "M", + } = options; + + try { + const qrData = generateQRData(chain, address, amount || undefined, label || undefined); + + const qrCodeDataUrl = await QRCodeLib.toDataURL(qrData, { + width, + margin, + errorCorrectionLevel, + type: "image/png" as const, + color: { + dark: darkColor, + light: lightColor, + }, + }); + + return { + dataUrl: qrCodeDataUrl, + rawData: qrData, + format: getQRFormat(chain), + }; + } catch (error) { + console.error("Error generating compatible QR code:", error); + throw new Error(`Failed to generate QR code for ${chain}: ${error instanceof Error ? error.message : String(error)}`); + } +}; + +/** + * Get the format description for a QR code + */ +export const getQRFormat = (chain: string): string => { + const normalizedChain = chain.toLowerCase(); + + const formatMap: Record = { + bitcoin: "BIP21 Bitcoin URI", + btc: "BIP21 Bitcoin URI", + ethereum: "EIP681 Ethereum URI", + eth: "EIP681 Ethereum URI", + solana: "Solana URI Scheme", + sol: "Solana URI Scheme", + starknet: "Ethereum-compatible URI", + strk: "Ethereum-compatible URI", + stellar: "Stellar URI Scheme", + xlm: "Stellar URI Scheme", + polkadot: "Polkadot URI Scheme", + dot: "Polkadot URI Scheme", + usdt_erc20: "ERC-20 Token URI", + usdt_erc: "ERC-20 Token URI", + usdt_trc20: "TRC-20 Token URI", + usdt_trc: "TRC-20 Token URI", + erc20: "ERC-20 Token URI", + trc20: "TRC-20 Token URI", + }; + + return formatMap[normalizedChain] || "Plain Address"; +}; + +/** + * Get block explorer URL for a transaction + */ +export const getBlockExplorerUrl = ( + chain: string, + txHash: string, + network: string = "mainnet" +): string => { + const normalizedChain = chain.toLowerCase(); + const normalizedNetwork = network.toLowerCase(); + const trimmedTxHash = txHash.trim(); + + const explorerUrls: Record = { + ethereum: { + testnet: `https://sepolia.etherscan.io/tx/${trimmedTxHash}`, + mainnet: `https://etherscan.io/tx/${trimmedTxHash}`, + }, + usdt_erc20: { + testnet: `https://sepolia.etherscan.io/tx/${trimmedTxHash}`, + mainnet: `https://etherscan.io/tx/${trimmedTxHash}`, + }, + bitcoin: { + testnet: `https://blockstream.info/testnet/tx/${trimmedTxHash}`, + mainnet: `https://blockstream.info/tx/${trimmedTxHash}`, + }, + solana: { + testnet: `https://explorer.solana.com/tx/${trimmedTxHash}?cluster=devnet`, + mainnet: `https://explorer.solana.com/tx/${trimmedTxHash}`, + }, + starknet: { + testnet: `https://sepolia.voyager.online/tx/${trimmedTxHash}`, + mainnet: `https://voyager.online/tx/${trimmedTxHash}`, + }, + stellar: { + testnet: `https://testnet.steexp.com/tx/${trimmedTxHash}`, + mainnet: `https://steexp.com/tx/${trimmedTxHash}`, + }, + polkadot: { + testnet: `https://polkadot.subscan.io/extrinsic/${trimmedTxHash}?network=westend`, + mainnet: `https://polkadot.subscan.io/extrinsic/${trimmedTxHash}`, + }, + usdt_trc20: { + testnet: `https://shasta.tronscan.org/#/transaction/${trimmedTxHash}`, + mainnet: `https://tronscan.org/#/transaction/${trimmedTxHash}`, + }, + }; + + const explorer = explorerUrls[normalizedChain]; + + if (!explorer) { + console.warn(`No explorer URL configured for chain: ${chain}`); + return "#"; + } + + const url = normalizedNetwork === "testnet" + ? explorer.testnet + : explorer.mainnet; + + return url || "#"; +}; + +/** + * Generate a simple QR code for any text/URL + */ +export const generateSimpleQRCode = async ( + text: string, + options: Omit = {} +): Promise => { + const { + width = 200, + margin = 2, + darkColor = "#000000", + lightColor = "#FFFFFF", + errorCorrectionLevel = "M", + } = options; + + try { + return await QRCodeLib.toDataURL(text, { + width, + margin, + errorCorrectionLevel, + type: "image/png" as const, + color: { + dark: darkColor, + light: lightColor, + }, + }); + } catch (error) { + console.error("Error generating simple QR code:", error); + throw error; + } +}; + +/** + * Validate if a chain supports QR URI schemes with amount + */ +export const supportsAmountInQR = (chain: string): boolean => { + const supportedChains = [ + "bitcoin", "btc", + "ethereum", "eth", + "solana", "sol", + "starknet", "strk", + "stellar", "xlm", + "polkadot", "dot", + "usdt_erc20", "usdt_erc", + "usdt_trc20", "usdt_trc", + "erc20", "trc20", + ]; + + return supportedChains.includes(chain.toLowerCase()); +}; + +/** + * Get appropriate currency symbol for amount display + */ +export const getCurrencySymbol = (chain: string): string => { + const symbolMap: Record = { + bitcoin: "BTC", + btc: "BTC", + ethereum: "ETH", + eth: "ETH", + solana: "SOL", + sol: "SOL", + starknet: "STRK", + strk: "STRK", + stellar: "XLM", + xlm: "XLM", + polkadot: "DOT", + dot: "DOT", + usdt_erc20: "USDT", + usdt_erc: "USDT", + usdt_trc20: "USDT", + usdt_trc: "USDT", + }; + + return symbolMap[chain.toLowerCase()] || chain.toUpperCase(); +}; \ No newline at end of file diff --git a/lib/utils/storage-utils.ts b/lib/utils/storage-utils.ts new file mode 100644 index 0000000..eecde1c --- /dev/null +++ b/lib/utils/storage-utils.ts @@ -0,0 +1,82 @@ +// Single source for all storage operations +export class StorageManager { + private static readonly PREFIX = 'velo'; + + static get(key: string, fallback: T | null = null): T | null { + if (typeof window === 'undefined') return fallback; + + try { + const fullKey = `${this.PREFIX}.${key}`; + const raw = sessionStorage.getItem(fullKey); + if (!raw) return fallback; + return JSON.parse(raw) as T; + } catch (e) { + console.error(`Error reading ${key} from storage:`, e); + return fallback; + } + } + + static set(key: string, value: T): void { + if (typeof window === 'undefined') return; + + try { + const fullKey = `${this.PREFIX}.${key}`; + sessionStorage.setItem(fullKey, JSON.stringify(value)); + } catch (e) { + console.error(`Error storing ${key} in storage:`, e); + } + } + + static remove(key: string): void { + if (typeof window === 'undefined') return; + + try { + const fullKey = `${this.PREFIX}.${key}`; + sessionStorage.removeItem(fullKey); + } catch (e) { + console.error(`Error removing ${key} from storage:`, e); + } + } + + static clearAll(): void { + if (typeof window === 'undefined') return; + + try { + const keys = Object.keys(sessionStorage); + keys.forEach(key => { + if (key.startsWith(`${this.PREFIX}.`)) { + sessionStorage.removeItem(key); + } + }); + } catch (e) { + console.error('Error clearing storage:', e); + } + } +} + +// Memory cache (replace duplicate implementations) +export const memoryCache = new Map< + string, + { data: any; timestamp: number; ttl: number } +>(); + +export const getMemoryCache = (key: string): T | null => { + const cached = memoryCache.get(key); + if (!cached) return null; + + const isExpired = Date.now() - cached.timestamp > cached.ttl; + if (isExpired) { + memoryCache.delete(key); + return null; + } + + return cached.data; +}; + +export const setMemoryCache = (key: string, data: T, ttl: number): void => { + memoryCache.set(key, { + data, + timestamp: Date.now(), + ttl, + }); +}; \ No newline at end of file diff --git a/lib/utils/token-utils.ts b/lib/utils/token-utils.ts new file mode 100644 index 0000000..94a1a88 --- /dev/null +++ b/lib/utils/token-utils.ts @@ -0,0 +1,274 @@ +export const normalizeChain = (raw: string | undefined | null): string => { + if (!raw) return ""; + + const k = String(raw).toLowerCase().trim(); + + if (k === "sol" || k === "solana") return "solana"; + if (k === "eth" || k === "ethereum") return "ethereum"; + if (k === "btc" || k === "bitcoin") return "bitcoin"; + if (k === "strk" || k === "starknet") return "starknet"; + if (k === "usdt" || k === "usdt_erc20" || k === "usdt-erc20") return "usdt_erc20"; + if (k === "usdt_trc20" || k === "usdt-trc20") return "usdt_trc20"; + if (k === "dot" || k === "polkadot") return "polkadot"; + if (k === "xlm" || k === "stellar") return "stellar"; + + // Partial matches (starts with) + if (k.startsWith("sol")) return "solana"; + if (k.startsWith("eth")) return "ethereum"; + if (k.startsWith("btc")) return "bitcoin"; + if (k.startsWith("strk")) return "starknet"; + if (k.startsWith("usdt")) return "usdt_erc20"; + if (k.startsWith("dot")) return "polkadot"; + if (k.startsWith("xlm")) return "stellar"; + + return k; +}; + +export const getTokenSymbol = (chain: string): string => { + if (!chain) return ""; + + const key = chain.toLowerCase(); + const symbolMap: Record = { + ethereum: "ETH", + bitcoin: "BTC", + solana: "SOL", + starknet: "STRK", + usdt_erc20: "USDT", + usdt_trc20: "USDT", + polkadot: "DOT", + stellar: "XLM", + // Add more mappings as needed + tron: "TRX", + polygon: "MATIC", + avalanche: "AVAX", + arbitrum: "ARB", + optimism: "OP", + }; + + return symbolMap[key] || chain.toUpperCase(); +}; + +export const getTokenName = (chain: string): string => { + if (!chain) return ""; + + const key = chain.toLowerCase(); + const nameMap: Record = { + ethereum: "Ethereum", + bitcoin: "Bitcoin", + solana: "Solana", + starknet: "Starknet", + usdt_erc20: "USDT (ERC20)", + usdt_trc20: "USDT (TRC20)", + polkadot: "Polkadot", + stellar: "Stellar", + tron: "TRON", + polygon: "Polygon", + avalanche: "Avalanche", + arbitrum: "Arbitrum", + optimism: "Optimism", + }; + + return nameMap[key] || chain.charAt(0).toUpperCase() + chain.slice(1); +}; + +export const getRateKey = (symbol: string): string => { + if (!symbol) return "USDT"; + + const symbolUpper = symbol.toUpperCase(); + const rateKeyMap: Record = { + ETH: "ETH", + BTC: "BTC", + SOL: "SOL", + STRK: "STRK", + USDT: "USDT", + USDC: "USDC", + DAI: "DAI", + DOT: "DOT", + XLM: "XLM", + MATIC: "MATIC", + AVAX: "AVAX", + ARB: "ARB", + OP: "OP", + TRX: "TRX", + }; + + return rateKeyMap[symbolUpper] || "USDT"; +}; + +// Get blockchain explorer URL for a token +export const getExplorerUrl = ( + chain: string, + txHash: string, + network: "testnet" | "mainnet" = "mainnet" +): string => { + const explorerUrls: Record> = { + ethereum: { + testnet: `https://sepolia.etherscan.io/tx/${txHash}`, + mainnet: `https://etherscan.io/tx/${txHash}`, + }, + bitcoin: { + testnet: `https://blockstream.info/testnet/tx/${txHash}`, + mainnet: `https://blockstream.info/tx/${txHash}`, + }, + solana: { + testnet: `https://explorer.solana.com/tx/${txHash}?cluster=devnet`, + mainnet: `https://explorer.solana.com/tx/${txHash}`, + }, + starknet: { + testnet: `https://sepolia.voyager.online/tx/${txHash}`, + mainnet: `https://voyager.online/tx/${txHash}`, + }, + polkadot: { + testnet: `https://polkascan.io/testnet/polkadot/transaction/${txHash}`, + mainnet: `https://polkascan.io/polkadot/transaction/${txHash}`, + }, + stellar: { + testnet: `https://stellar.expert/explorer/testnet/tx/${txHash}`, + mainnet: `https://stellar.expert/explorer/public/tx/${txHash}`, + }, + }; + + const normalizedChain = normalizeChain(chain); + const explorer = explorerUrls[normalizedChain]; + + if (!explorer) { + // Fallback to generic or chain-specific default + return `https://explorer.example.com/tx/${txHash}`; + } + + return explorer[network]; +}; + +// Check if chain supports smart contracts +export const supportsSmartContracts = (chain: string): boolean => { + const normalizedChain = normalizeChain(chain); + const contractChains = new Set([ + "ethereum", + "starknet", + "solana", + "polygon", + "avalanche", + "arbitrum", + "optimism", + "tron", + ]); + + return contractChains.has(normalizedChain); +}; + +// Get decimal places for a token +export const getTokenDecimals = (chain: string): number => { + const normalizedChain = normalizeChain(chain); + + const decimalsMap: Record = { + ethereum: 18, + bitcoin: 8, + solana: 9, + starknet: 18, + polkadot: 10, + stellar: 7, + tron: 6, + usdt_erc20: 6, + usdt_trc20: 6, + }; + + return decimalsMap[normalizedChain] || 18; +}; + +// Format token amount with proper decimals +export const formatTokenAmount = ( + amount: number | string, + chain: string, + options?: { + maxDecimals?: number; + showSymbol?: boolean; + } +): string => { + const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount; + const decimals = getTokenDecimals(chain); + const maxDecimals = options?.maxDecimals || Math.min(decimals, 8); + + let formatted: string; + + if (numAmount === 0) { + formatted = "0"; + } else if (numAmount < Math.pow(10, -maxDecimals)) { + formatted = `<0.${'0'.repeat(maxDecimals - 1)}1`; + } else { + formatted = numAmount.toFixed(maxDecimals).replace(/\.?0+$/, ''); + } + + if (options?.showSymbol) { + const symbol = getTokenSymbol(chain); + return `${formatted} ${symbol}`; + } + + return formatted; +}; + +// Convert between chain representations +export const chainToSymbol = (chain: string): string => getTokenSymbol(chain); +export const symbolToChain = (symbol: string): string => { + const symbolUpper = symbol.toUpperCase(); + const reverseMap: Record = { + ETH: "ethereum", + BTC: "bitcoin", + SOL: "solana", + STRK: "starknet", + USDT: "usdt_erc20", + DOT: "polkadot", + XLM: "stellar", + TRX: "tron", + MATIC: "polygon", + AVAX: "avalanche", + ARB: "arbitrum", + OP: "optimism", + }; + + return reverseMap[symbolUpper] || symbol.toLowerCase(); +}; + + + +export const getTokenRateKey = (token: string): string => { + const rateMap: Record = { + ETHEREUM: "ETH", + BITCOIN: "BTC", + SOLANA: "SOL", + STARKNET: "STRK", + USDT_TRC20: "USDT", + USDT_ERC20: "USDT", + POLKADOT: "DOT", + STELLAR: "XLM", + }; + return rateMap[token] || "USDT"; +}; + +export const getTokenChain = (token: string): string => { + const chainMap: Record ={ + ETHEREUM: "ethereum", + BITCOIN: "bitcoin", + SOLANA: "solana", + STARKNET: "starknet", + USDT_ERC20: "usdt_erc20", + USDT_TRC20: "usdt_trc20", + POLKADOT: "polkadot", + STELLAR: "stellar", + }; + return chainMap[token] || "ethereum"; +}; + +export const formatBalance = (balance: number): string => { + if (balance === 0) return "0.00"; + if (balance < 0.001) return "<0.001"; + return balance.toFixed(4); +}; + +export const formatNGN = (amount: number): string => { + return new Intl.NumberFormat("en-NG", { + style: "currency", + currency: "NGN", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(amount); +}; diff --git a/package-lock.json b/package-lock.json index 8b15aa6..41395f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "bip32": "^5.0.0-rc.0", "bip39": "^3.1.0", "bitcoinjs-lib": "^6.1.7", + "chroma-js": "^3.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cors": "^2.8.5", @@ -61,6 +62,7 @@ "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", "@types/bcrypt": "^6.0.0", + "@types/chroma-js": "^3.1.2", "@types/crypto-js": "^4.2.2", "@types/node": "^20", "@types/nodemailer": "^7.0.3", @@ -5875,6 +5877,13 @@ "@types/node": "*" } }, + "node_modules/@types/chroma-js": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-3.1.2.tgz", + "integrity": "sha512-YBTQqArPN8A0niHXCwrO1z5x++a+6l0mLBykncUpr23oIPW7L4h39s6gokdK/bDrPmSh8+TjMmrhBPnyiaWPmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -10273,6 +10282,12 @@ "node": ">=18" } }, + "node_modules/chroma-js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.2.0.tgz", + "integrity": "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw==", + "license": "(BSD-3-Clause AND Apache-2.0)" + }, "node_modules/cipher-base": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", @@ -13482,9 +13497,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -14515,9 +14530,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz", - "integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -16834,11 +16849,11 @@ } }, "node_modules/tar": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", - "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", diff --git a/package.json b/package.json index 18cb750..14b5e70 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "bip32": "^5.0.0-rc.0", "bip39": "^3.1.0", "bitcoinjs-lib": "^6.1.7", + "chroma-js": "^3.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cors": "^2.8.5", @@ -63,6 +64,7 @@ "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", "@types/bcrypt": "^6.0.0", + "@types/chroma-js": "^3.1.2", "@types/crypto-js": "^4.2.2", "@types/node": "^20", "@types/nodemailer": "^7.0.3", diff --git a/types/hooks.ts b/types/hooks.ts new file mode 100644 index 0000000..11af4ac --- /dev/null +++ b/types/hooks.ts @@ -0,0 +1,12 @@ +export interface QueryResult { + data: T | null; + error: string | null; + isLoading: boolean; + refetch: () => Promise; +} + +export interface PaginatedQueryResult extends QueryResult { + hasMore: boolean; + loadMore: () => Promise; + isLoadingMore: boolean; +} \ No newline at end of file From c089d09e0dd37b899d90179a32425d7487f4e46d Mon Sep 17 00:00:00 2001 From: tali-creator Date: Thu, 4 Dec 2025 04:00:55 +0100 Subject: [PATCH 7/9] refactored --- components/dashboard/tabs/airtime.tsx | 24 - components/dashboard/tabs/create-address.tsx | 352 ----------- components/dashboard/tabs/dashboard.tsx | 93 --- components/dashboard/tabs/data.tsx | 8 - components/dashboard/tabs/electricity.tsx | 24 - components/dashboard/tabs/help.tsx | 562 ------------------ components/dashboard/tabs/history.tsx | 361 ------------ components/dashboard/tabs/logout.tsx | 23 - components/dashboard/tabs/notifications.tsx | 303 ---------- components/dashboard/tabs/payment-split.tsx | 222 ------- components/dashboard/tabs/profile.tsx | 8 - components/dashboard/tabs/qr-payment.tsx | 541 ----------------- components/dashboard/tabs/send-funds.tsx | 589 ------------------- components/dashboard/tabs/services.tsx | 126 ---- components/dashboard/tabs/swap.tsx | 463 --------------- components/dashboard/tabs/top-up.tsx | 143 ----- 16 files changed, 3842 deletions(-) delete mode 100644 components/dashboard/tabs/airtime.tsx delete mode 100644 components/dashboard/tabs/create-address.tsx delete mode 100644 components/dashboard/tabs/dashboard.tsx delete mode 100644 components/dashboard/tabs/data.tsx delete mode 100644 components/dashboard/tabs/electricity.tsx delete mode 100644 components/dashboard/tabs/help.tsx delete mode 100644 components/dashboard/tabs/history.tsx delete mode 100644 components/dashboard/tabs/logout.tsx delete mode 100644 components/dashboard/tabs/notifications.tsx delete mode 100644 components/dashboard/tabs/payment-split.tsx delete mode 100644 components/dashboard/tabs/profile.tsx delete mode 100644 components/dashboard/tabs/qr-payment.tsx delete mode 100644 components/dashboard/tabs/send-funds.tsx delete mode 100644 components/dashboard/tabs/services.tsx delete mode 100644 components/dashboard/tabs/swap.tsx delete mode 100644 components/dashboard/tabs/top-up.tsx diff --git a/components/dashboard/tabs/airtime.tsx b/components/dashboard/tabs/airtime.tsx deleted file mode 100644 index 44c5dea..0000000 --- a/components/dashboard/tabs/airtime.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client" - -import React, { useEffect } from 'react' -import Purchase from '../service-flow' - -export default function Airtime() { - - const fetchProvider = async () => { - try{ - const res = await fetch("https://www.nellobytesystems.com/APIAirtimeDiscountV2.asp?UserID=CK101265322") - const data = await res.json() - console.log(data) - }catch(err){ - console.log(err) - } - } - - useEffect(() => { - fetchProvider() - }, []) - return ( - - ) -} diff --git a/components/dashboard/tabs/create-address.tsx b/components/dashboard/tabs/create-address.tsx deleted file mode 100644 index 916260c..0000000 --- a/components/dashboard/tabs/create-address.tsx +++ /dev/null @@ -1,352 +0,0 @@ -"use client"; - -import { Card } from "@/components/ui/Card"; -import { ChevronDown, Copy, Check, Loader2 } from "lucide-react"; -import React, { useState, useCallback, useEffect, ReactElement } from "react"; -import { fixStarknetAddress, shortenAddress } from "@/components/lib/utils"; -import Image from "next/image"; -import QRCodeLib from "qrcode"; -import { AddressDropdown } from "@/components/modals/addressDropDown"; -import { useWalletData } from "@/components/hooks/useWalletData"; -interface TokenOption { - // symbol: ReactElement; - name: string; - walletAddress: string; -} - -// QR code format generators for different cryptocurrencies -const generateQRData = ( - chain: string, - address: string, - amount: string | null = null, - label: string | null = null -): string => { - switch (chain.toLowerCase()) { - case "bitcoin": - case "btc": - let bitcoinUri = `bitcoin:${address}`; - const bitcoinParams = []; - if (amount) bitcoinParams.push(`amount=${amount}`); - if (label) bitcoinParams.push(`label=${encodeURIComponent(label)}`); - if (bitcoinParams.length > 0) { - bitcoinUri += `?${bitcoinParams.join("&")}`; - } - return bitcoinUri; - - case "ethereum": - case "eth": - let ethereumUri = `ethereum:${address}`; - const ethereumParams = []; - if (amount) { - const weiAmount = (parseFloat(amount) * Math.pow(10, 18)).toString(); - ethereumParams.push(`value=${weiAmount}`); - } - if (label) ethereumParams.push(`label=${encodeURIComponent(label)}`); - if (ethereumParams.length > 0) { - ethereumUri += `?${ethereumParams.join("&")}`; - } - return ethereumUri; - - case "solana": - case "sol": - let solanaUri = `solana:${address}`; - const solanaParams = []; - if (amount) solanaParams.push(`amount=${amount}`); - if (label) solanaParams.push(`label=${encodeURIComponent(label)}`); - if (solanaParams.length > 0) { - solanaUri += `?${solanaParams.join("&")}`; - } - return solanaUri; - - case "starknet": - case "strk": - let starknetUri = `starknet:${address}`; - const starknetParams = []; - if (amount) { - const weiAmount = (parseFloat(amount) * Math.pow(10, 18)).toString(); - starknetParams.push(`value=${weiAmount}`); - } - if (label) starknetParams.push(`label=${encodeURIComponent(label)}`); - if (starknetParams.length > 0) { - starknetUri += `?${starknetParams.join("&")}`; - } - return starknetUri; - - case "erc20": - return `ethereum:${address}`; - case "trc20": - return `tron:${address}`; - - default: - return address; - } -}; - -// Enhanced QR code generation function -const generateCompatibleQRCode = async ( - chain: string, - address: string, - options: { - amount?: string | null; - label?: string | null; - width?: number; - margin?: number; - darkColor?: string; - lightColor?: string; - errorCorrectionLevel?: "L" | "M" | "Q" | "H"; - } = {} -) => { - const { - amount = null, - label = null, - width = 200, - margin = 2, - darkColor = "#000000", - lightColor = "#FFFFFF", - errorCorrectionLevel = "M", - } = options; - - try { - const qrData = generateQRData(chain, address, amount, label); - - const qrCodeDataUrl = await QRCodeLib.toDataURL(qrData, { - width, - margin, - errorCorrectionLevel, - type: "image/png" as "image/png" | "image/jpeg" | "image/webp", - color: { - dark: darkColor, - light: lightColor, - }, - }); - - return { - dataUrl: qrCodeDataUrl, - rawData: qrData, - format: getQRFormat(chain), - }; - } catch (error) { - console.error("Error generating compatible QR code:", error); - throw error; - } -}; - -// Helper function to get the format description -const getQRFormat = (chain: string): string => { - switch (chain.toLowerCase()) { - case "bitcoin": - case "btc": - return "BIP21 Bitcoin URI"; - case "ethereum": - case "eth": - return "EIP681 Ethereum URI"; - case "solana": - case "sol": - return "Solana URI Scheme"; - case "starknet": - case "strk": - return "Ethereum-compatible URI"; - case "erc20": - return "ERC-20 Token URI"; - case "trc20": - return "TRC-20 Token URI"; - default: - return "Plain Address"; - } -}; - -export default function ReceiveFunds() { - const [selectedToken, setSelectedToken] = useState("starknet"); - const [showDropdown, setShowDropdown] = useState(false); - const [qrData, setQrData] = useState(""); - const [copied, setCopied] = useState(false); - const [loading, setLoading] = useState(true); - - const { addresses } = useWalletData(); - - - // Check if wallet addresses are available before rendering - useEffect(() => { - if (addresses && addresses.length > 0) { - setLoading(false); - } - }, [addresses]); - - const selectedTokenData = addresses?.find( - (token) => token.chain === selectedToken - ); - - // Generate QR code when selected token changes using enhanced function - useEffect(() => { - if (selectedTokenData && selectedTokenData?.address) { - const generateQrCode = async () => { - try { - let addressToUse = selectedTokenData?.address; - if (selectedTokenData.chain.toLowerCase() === "starknet") { - addressToUse = fixStarknetAddress( - addressToUse, - selectedTokenData.chain - ); - } - - const qrResult = await generateCompatibleQRCode( - selectedTokenData.chain, - addressToUse, - { - width: 200, - margin: 2, - errorCorrectionLevel: "M", - } - ); - - setQrData(qrResult.dataUrl); - } catch (error) { - console.error("Error generating QR code:", error); - } - }; - - generateQrCode(); - } - }, [selectedTokenData]); - - const handleTokenSelect = useCallback((symbol: string) => { - setSelectedToken(symbol); - setShowDropdown(false); - }, []); - - const handleCopyAddress = useCallback(async () => { - if (!selectedTokenData) return; - - try { - await navigator.clipboard.writeText( - fixStarknetAddress(selectedTokenData?.address, selectedTokenData.chain) - ); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (err) { - console.error("Failed to copy address: ", err); - } - }, [selectedTokenData]); - - // Close dropdown when clicking outside - useEffect(() => { - const handleClickOutside = () => { - if (showDropdown) { - setShowDropdown(false); - } - }; - - document.addEventListener("click", handleClickOutside); - return () => { - document.removeEventListener("click", handleClickOutside); - }; - }, [showDropdown]); - - // Show loading state while addresses are being fetched - if (addresses.length < 1) { - return ( -
-
- -

Loading wallet addresses...

-
-
- ); - } - - // Show error state if no addresses are available - if (!addresses || addresses.length === 0) { - return ( -
- -

- No Wallet Addresses -

-

- Unable to retrieve wallet addresses. Please check your connection - and try again. -

-
-
- ); - } - - return ( -
-
- - {/* Header */} -
-

Receive Funds

-

- Select a currency and share your address to receive payments -

-
- - {/* Token Selector */} - - - - {/* QR Code */} -
- {qrData ? ( -
- QR Code -
- ) : ( -
- Loading QR... -
- )} - -
-

- {selectedToken} Address -

-
-

- {selectedTokenData?.address - ? shortenAddress( - fixStarknetAddress( - selectedTokenData.address, - selectedTokenData.chain - ), - 10 - ) - : ""} -

- -
-
-
- - {/* Instructions */} -
- - -

- How to receive funds -

-
    -
  • Select the currency you want to receive
  • -
  • Share your QR code or wallet address
  • -
  • Wait for the sender to complete the transaction
  • -
  • Funds will appear in your wallet after confirmation
  • -
-
-
-
- ); -} diff --git a/components/dashboard/tabs/dashboard.tsx b/components/dashboard/tabs/dashboard.tsx deleted file mode 100644 index 61dfe08..0000000 --- a/components/dashboard/tabs/dashboard.tsx +++ /dev/null @@ -1,93 +0,0 @@ -"use client"; - -import { Card } from "@/components/ui/Card"; -import Button from "@/components/ui/Button"; -import { StatsCards } from "../stats-cards"; -import { QuickActions } from "../quick-actions"; -import { RecentActivity } from "../recent-activity"; -import { WalletOverview } from "../wallet-overview"; -import { useAuth } from "@/components/context/AuthContext"; -import { useState } from "react"; - -interface RecentActivity { - id: string; - type: "incoming" | "outgoing" | "swap" | "split"; - amount: string; - token: string; - description: string; - timestamp: string; - status: "completed" | "pending" | "failed"; -} - -export interface DashboardProps { - activeTab: React.Dispatch>; -} - -export default function DashboardHome({ activeTab }: DashboardProps) { - const { user } = useAuth(); - - const [hideBalalance, setHideBalance] = useState(false); - - const handleViewBalance = () => { - setHideBalance(!hideBalalance); - }; - - return ( -
- {/* Header */} -
-

- Welcome back, {user?.firstName?.toLocaleUpperCase()} -

-

- {"Ready to manage your finances? Let's make some magic happen."} -

-
- - {/* Stats Grid */} - - - {/* Quick Actions */} -
- {" "} -
- - {/* Main Content Grid */} -
- - -
- -
-
- - {/* Bottom CTA */} - -
-
-

Need Help?

-

- Check our Help or contact support -

-
- -
- - -
-
-
-
- ); -} diff --git a/components/dashboard/tabs/data.tsx b/components/dashboard/tabs/data.tsx deleted file mode 100644 index 4000e8e..0000000 --- a/components/dashboard/tabs/data.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' -import Purchase from '../service-flow' - -export default function Data() { - return ( - - ) -} diff --git a/components/dashboard/tabs/electricity.tsx b/components/dashboard/tabs/electricity.tsx deleted file mode 100644 index 3dce95e..0000000 --- a/components/dashboard/tabs/electricity.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client" - -import React, { useEffect } from 'react' -import Purchase from '@/components/dashboard/service-flow' - -export default function Electricity() { - - const fetchProvider = async () => { - try{ - const res = await fetch("https://www.nellobytesystems.com/APIAirtimeDiscountV2.asp?UserID=CK101265322") - const data = await res.json() - console.log(data) - }catch(err){ - console.log(err) - } - } - - useEffect(() => { - fetchProvider() - }, []) - return ( - - ) -} diff --git a/components/dashboard/tabs/help.tsx b/components/dashboard/tabs/help.tsx deleted file mode 100644 index 58d1d98..0000000 --- a/components/dashboard/tabs/help.tsx +++ /dev/null @@ -1,562 +0,0 @@ -"use client"; - -import { Card } from "@/components/ui/Card"; -import { - Search, - MessageCircle, - BookOpen, - HelpCircle, - Mail, - Globe, - FileText, -} from "lucide-react"; -import React, { useState, useEffect } from "react"; -import Button from "@/components/ui/Button"; - -interface FAQItem { - id: string; - question: string; - answer: string; - category: string; -} - -interface Article { - id: string; - title: string; - description: string; - category: string; - readTime: string; -} - -interface ContactOption { - id: string; - title: string; - description: string; - icon: React.ReactNode; - action: string; -} - -export default function Help() { - const [searchQuery, setSearchQuery] = useState(""); - const [activeCategory, setActiveCategory] = useState("all"); - const [currentPage, setCurrentPage] = useState(1); - const [itemsPerPage] = useState(5); - const [displayedPages, setDisplayedPages] = useState([]); - const [totalPages, setTotalPages] = useState(0); - - // FAQ data - const faqs: FAQItem[] = [ - { - id: "1", - question: "How do I create a payment split?", - answer: - "To create a payment split, go to the 'Split' tab, click the '+' Button, add recipients with their percentages, and confirm the split creation.", - category: "payments", - }, - { - id: "2", - question: "What tokens are supported for swapping?", - answer: - "We currently support USDT, USDC, STRK, and ETH for swapping to Naira.", - category: "swap", - }, - { - id: "3", - question: "How do I generate a QR code for payments?", - answer: - "Navigate to the 'QRPayment' tab, enter the amount in Naira, select a token, and click 'Create Payment Request' to generate your QR code.", - category: "payments", - }, - { - id: "4", - question: "How long do transactions take to process?", - answer: - "Most transactions are processed within 2-5 minutes, but Starknet transactions can sometimes take up to 15 minutes during high network congestion.", - category: "general", - }, - { - id: "5", - question: "How do I connect my wallet?", - answer: - "Click the 'Connect Wallet' Button in the header and select your preferred wallet provider (ArgentX or Braavos).", - category: "wallets", - }, - { - id: "6", - question: "What are the transaction fees?", - answer: - "We charge a 0.5% fee on all transactions. Network (gas) fees are additional and vary based on blockchain congestion.", - category: "fees", - }, - { - id: "7", - question: "How do I export my transaction history?", - answer: - "Go to the 'History' tab and click the 'Export CSV' Button to download your transaction history.", - category: "history", - }, - { - id: "8", - question: "Can I use testnet and mainnet?", - answer: - "Yes, your wallet addresses work on both testnet and mainnet. Switching networks only changes which blockchain you're viewing.", - category: "wallets", - }, - { - id: "9", - question: "How do I update my profile information?", - answer: - "Navigate to the 'Profile' tab to edit your personal information and linked bank accounts.", - category: "profile", - }, - { - id: "10", - question: "What should I do if a transaction fails?", - answer: - "Failed transactions typically refund automatically. If you don't receive a refund within 24 hours, please contact our support team.", - category: "troubleshooting", - }, - { - id: "11", - question: "How secure is my data?", - answer: - "We use industry-standard encryption and security practices. Your private keys never leave your device, and we don't have access to your funds.", - category: "security", - }, - { - id: "12", - question: "Can I use multiple bank accounts?", - answer: - "Yes, you can link multiple bank accounts in your profile settings and select which one to use for transactions.", - category: "banking", - }, - ]; - - // Help articles - const articles: Article[] = [ - { - id: "1", - title: "Getting Started Guide", - description: - "A comprehensive guide to setting up your account and making your first transaction.", - category: "guides", - readTime: "5 min read", - }, - { - id: "2", - title: "Understanding Transaction Fees", - description: - "Learn about our fee structure and how to minimize transaction costs.", - category: "fees", - readTime: "3 min read", - }, - { - id: "3", - title: "Troubleshooting Common Issues", - description: "Solutions for the most frequently encountered problems.", - category: "troubleshooting", - readTime: "7 min read", - }, - { - id: "4", - title: "Security Best Practices", - description: "How to keep your account and funds secure.", - category: "security", - readTime: "4 min read", - }, - { - id: "5", - title: "Advanced Split Configurations", - description: "How to set up complex payment distribution scenarios.", - category: "payments", - readTime: "6 min read", - }, - ]; - - // Contact options - const contactOptions: ContactOption[] = [ - { - id: "1", - title: "Live Chat", - description: "Get immediate assistance from our support team", - icon: , - action: "Start Chat", - }, - { - id: "2", - title: "Email Support", - description: - "Send us a detailed message and we'll respond within 24 hours", - icon: , - action: "Send Email", - }, - { - id: "3", - title: "Knowledge Base", - description: "Browse our comprehensive documentation and guides", - icon: , - action: "Browse Articles", - }, - ]; - - // Categories for filtering - const categories = [ - { id: "all", name: "All Topics" }, - { id: "payments", name: "Payments" }, - { id: "wallets", name: "Wallets" }, - { id: "swap", name: "Swap" }, - { id: "fees", name: "Fees" }, - { id: "security", name: "Security" }, - { id: "troubleshooting", name: "Troubleshooting" }, - ]; - - // Filter FAQs based on search query and active category - const filteredFaqs = faqs.filter( - (faq) => - (faq.question.toLowerCase().includes(searchQuery.toLowerCase()) || - faq.answer.toLowerCase().includes(searchQuery.toLowerCase())) && - (activeCategory === "all" || faq.category === activeCategory) - ); - - // Get current items for pagination - const indexOfLastItem = currentPage * itemsPerPage; - const indexOfFirstItem = indexOfLastItem - itemsPerPage; - const currentItems = filteredFaqs.slice(indexOfFirstItem, indexOfLastItem); - - useEffect(() => { - // Calculate total pages - const total = Math.ceil(filteredFaqs.length / itemsPerPage); - setTotalPages(total); - - // Update displayed pages - updateDisplayedPages(currentPage, total); - }, [currentPage, searchQuery, activeCategory, filteredFaqs.length]); - - const updateDisplayedPages = (page: number, total: number) => { - if (total <= 1) { - setDisplayedPages([]); - return; - } - - const pages: number[] = []; - const maxVisible = 5; - let startPage = Math.max(1, page - Math.floor(maxVisible / 2)); - let endPage = startPage + maxVisible - 1; - - if (endPage > total) { - endPage = total; - startPage = Math.max(1, endPage - maxVisible + 1); - } - - for (let i = startPage; i <= endPage; i++) { - pages.push(i); - } - - setDisplayedPages(pages); - }; - - const handlePageChange = (pageNumber: number) => { - if (pageNumber < 1 || pageNumber > totalPages) return; - setCurrentPage(pageNumber); - }; - - const renderPageNumbers = () => { - if (totalPages <= 1) return null; - - return ( - <> - {displayedPages[0] > 1 && ( - <> - - {displayedPages[0] > 2 && ( - ... - )} - - )} - - {displayedPages.map((number) => ( - - ))} - - {displayedPages[displayedPages.length - 1] < totalPages && ( - <> - {displayedPages[displayedPages.length - 1] < totalPages - 1 && ( - ... - )} - - - )} - - ); - }; - - // Live chat would typically open a chat widget; here we just alert for demo - const handleContactAction = (actionType: string) => { - switch (actionType) { - case "Start Chat": - alert("Live chat would open here"); - break; - case "Send Email": - window.location.href = "mailto:support@yourplatform.com"; - break; - case "Browse Articles": - // Scroll to articles section - document - .getElementById("knowledge-base") - ?.scrollIntoView({ behavior: "smooth" }); - break; - default: - break; - } - }; - - return ( -
-
-

Help Center

-

- Find answers to common questions and get support -

- - {/* Search Section */} -
-
- - { - setSearchQuery(e.target.value); - setCurrentPage(1); - }} - className="w-full pl-10 pr-4 py-3 bg-background border border-border rounded-md text-foreground placeholder:text-muted-foreground" - /> -
-
- - {/* Category Filters */} -
- {categories.map((category) => ( - - ))} -
- - {/* Contact Options */} -
- {contactOptions.map((option) => ( - -
- {option.icon} -
-

- {option.title} -

-

- {option.description} -

- -
- ))} -
- - {/* FAQ Section */} -
-

- Frequently Asked Questions -

- - {currentItems.length > 0 ? ( -
- {currentItems.map((faq) => ( - -

- - {faq.question} -

-

- {faq.answer} -

-
- ))} -
- ) : ( - - -

No questions found

-

- Try a different search term or category -

-
- )} - - {/* Pagination for FAQs */} - {filteredFaqs.length > 0 && ( -
-
- Showing {indexOfFirstItem + 1} to{" "} - {Math.min(indexOfLastItem, filteredFaqs.length)} of{" "} - {filteredFaqs.length} results -
-
- - - {renderPageNumbers()} - - -
-
- )} -
- - {/* Knowledge Base Section (temporarily removed) -
-

- Knowledge Base -

-
- {articles.map((article) => ( - -
- -

- {article.title} -

-
-

- {article.description} -

-
- - {article.category} - - - {article.readTime} - -
-
- ))} -
-
- */} - - {/* Additional Help Section */} - -
-
- {/*

- Still need help? -

*/} -

- Still need help? - Our support team is available 24/7 to assist you with any issues - or questions. -

-
-
- - -
-
-
-
-
- ); -} diff --git a/components/dashboard/tabs/history.tsx b/components/dashboard/tabs/history.tsx deleted file mode 100644 index ab7f456..0000000 --- a/components/dashboard/tabs/history.tsx +++ /dev/null @@ -1,361 +0,0 @@ -"use client"; - -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/ui/cards"; -import { Button } from "@/components/ui/buttons"; -import { Badge } from "@/components/ui/badge"; -import { - ArrowUpRight, - ArrowDownLeft, - ArrowUpDown, - Users, - DollarSign, - ChevronRight, -} from "lucide-react"; -import React, { useState, useEffect } from "react"; -import { useTransactions } from "@/components/hooks/useTransactions"; - -declare global { - interface Date { - toRelativeTime(): string; - } -} - -// Add toRelativeTime method to Date prototype -Date.prototype.toRelativeTime = function () { - const now = new Date("2025-09-25T08:40:00+01:00"); - const diffMs = now.getTime() - this.getTime(); - const diffMinutes = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMs / 3600000); - const diffDays = Math.floor(diffMs / 86400000); - - if (diffMinutes < 60) return `${diffMinutes} min ago`; - if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`; - return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`; -}; - -interface Transaction { - id: string; - type: string; - chain: string; - amount: string; - currency: string; - fromAddress: string; - toAddress: string; - txHash: string; - status: string; - timestamp: string; -} - -const ActivityIcon = ({ type, status }: { type: string; status: string }) => { - const baseClasses = "p-2 hidden sm:flex rounded-full"; - - if (status === "pending") { - return ( -
-
-
- ); - } - - if (status === "failed") { - return
⚠️
; - } - - switch (type) { - case "incoming": - return ( -
- -
- ); - case "outgoing": - return ( -
- -
- ); - case "swap": - return ( -
- -
- ); - case "split": - return ( -
- -
- ); - default: - return ( -
- -
- ); - } -}; - -export default function History() { - const [searchQuery, setSearchQuery] = useState(""); - const [currentPage, setCurrentPage] = useState(1); - const [itemsPerPage] = useState(10); - const [displayedPages, setDisplayedPages] = useState([]); - const [totalPages, setTotalPages] = useState(0); - const { transactions, pagination, error, refetch } = useTransactions({ - page: currentPage, - limit: itemsPerPage, - }); - - const updateDisplayedPages = (page: number, total: number) => { - if (total <= 1) { - setDisplayedPages([]); - return; - } - - const pages: number[] = []; - const maxVisible = 5; - let startPage = Math.max(1, page - Math.floor(maxVisible / 2)); - let endPage = startPage + maxVisible - 1; - - if (endPage > total) { - endPage = total; - startPage = Math.max(1, endPage - maxVisible + 1); - } - - for (let i = startPage; i <= endPage; i++) { - pages.push(i); - } - - setDisplayedPages(pages); - }; - - useEffect(() => { - if (pagination) { - setTotalPages(pagination.totalPages || 0); - updateDisplayedPages(currentPage, pagination.totalPages || 0); - } - }, [pagination, currentPage]); - - const filteredTransactions = transactions.filter( - (tx) => - tx.fromAddress.toLowerCase().includes(searchQuery.toLowerCase()) || - tx.toAddress.toLowerCase().includes(searchQuery.toLowerCase()) || - tx.type.toLowerCase().includes(searchQuery.toLowerCase()) || - tx.status.toLowerCase().includes(searchQuery.toLowerCase()) - ); - - const handlePageChange = (pageNumber: number) => { - if (pageNumber < 1 || pageNumber > totalPages) return; - setCurrentPage(pageNumber); - }; - - const renderPageNumbers = () => { - if (totalPages <= 1) return null; - - return ( - <> - {displayedPages[0] > 1 && ( - <> - - {displayedPages[0] > 2 && ( - ... - )} - - )} - - {displayedPages.map((number) => ( - - ))} - - {displayedPages[displayedPages.length - 1] < totalPages && ( - <> - {displayedPages[displayedPages.length - 1] < totalPages - 1 && ( - ... - )} - - - )} - - ); - }; - - if (transactions.length === 0) { - // Render a nicer skeleton while initial transactions load (or no cached data) - return ( -
-
- {[...Array(6)].map((_, i) => ( -
-
-
-
-
-
-
-
-
-
-
-
- ))} -
-
- ); - } - - return ( -
-
- - - - Transactions History - - - - - {error && ( -
- Error: {error} - -
- )} - - {transactions.length < 1 ? ( -
-
- Loading... -
-
- ) : filteredTransactions.length === 0 ? ( // UPDATE: Use filteredTransactions -
- No transactions found -
- ) : ( - filteredTransactions.map((tx) => ( -
-
- -
-

- {tx.type.charAt(0).toUpperCase() + tx.type.slice(1)}{" "} - {tx.type === "swap" - ? `(${tx.fromAddress} to ${tx.toAddress})` - : ""} -

-

- {new Date(tx.timestamp).toRelativeTime()} -

-
-
-
-
-

- {tx.type === "incoming" ? "+" : "-"} - {tx.amount} {tx.currency} -

- - {tx.status} - -
-
-
- )) - )} -
-
- -
-
- Showing {filteredTransactions.length} of {transactions.length}{" "} - results -
-
- - - {renderPageNumbers()} - - -
-
-
-
- ); -} diff --git a/components/dashboard/tabs/logout.tsx b/components/dashboard/tabs/logout.tsx deleted file mode 100644 index 5df1ea2..0000000 --- a/components/dashboard/tabs/logout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useAuth } from "@/components/context/AuthContext"; -import React from "react"; - -export default function Logout() { - const { logout } = useAuth(); - - - return ( -
-
-

- Are you sure you want to logout? -

- -
-
- ); -} diff --git a/components/dashboard/tabs/notifications.tsx b/components/dashboard/tabs/notifications.tsx deleted file mode 100644 index 32fcd7a..0000000 --- a/components/dashboard/tabs/notifications.tsx +++ /dev/null @@ -1,303 +0,0 @@ -"use client"; - -import { Bell, Check } from "lucide-react"; -import React, { useState, useEffect, useMemo } from "react"; // Remove SetStateAction import -import { Card } from "@/components/ui/Card"; -import { useNotifications } from "@/components/hooks/useNotifications"; // Fixed path -import ViewNotificationDetails from "@/components/modals/view-notification-details"; -import { FrontendNotification } from "@/types"; - -export default function Notifications() { - const { - notifications, - error, - markAsRead, - markAllAsRead, - } = useNotifications(); - const [activeTab, setActiveTab] = useState<"today" | "this-week" | "earlier">( - "today" - ); - const [currentPage, setCurrentPage] = useState(1); - const [itemsPerPage] = useState(10); - const [showNotifDetails, setShowNotifDetails] = useState(false); - const [content, setContent] = useState(); - - // Use useMemo for filtered notifications to prevent unnecessary recalculations - const filteredNotifications = useMemo(() => { - return notifications.filter((notif) => notif.category === activeTab); - }, [notifications, activeTab]); - - // Use useMemo for pagination data - const paginationData = useMemo(() => { - const totalPages = Math.ceil(filteredNotifications.length / itemsPerPage); - const indexOfLastItem = currentPage * itemsPerPage; - const indexOfFirstItem = indexOfLastItem - itemsPerPage; - const currentItems = filteredNotifications.slice( - indexOfFirstItem, - indexOfLastItem - ); - - return { totalPages, indexOfLastItem, indexOfFirstItem, currentItems }; - }, [filteredNotifications, currentPage, itemsPerPage]); - - const { totalPages, indexOfLastItem, indexOfFirstItem, currentItems } = - paginationData; - - // Calculate displayed pages - const displayedPages = useMemo(() => { - const total = totalPages; - if (total <= 1) return []; - - const pages: number[] = []; - const maxVisible = 5; - let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2)); - let endPage = startPage + maxVisible - 1; - - if (endPage > total) { - endPage = total; - startPage = Math.max(1, endPage - maxVisible + 1); - } - - for (let i = startPage; i <= endPage; i++) { - pages.push(i); - } - - return pages; - }, [currentPage, totalPages]); - - const handleShowNotif = (id: string) => { - const selectedNotif = notifications.find((notif) => notif.id === id); - setContent(selectedNotif); - setShowNotifDetails(true); - }; - - const handleClearAll = () => { - setCurrentPage(1); - }; - - const handlePageChange = (pageNumber: number) => { - if (pageNumber < 1 || pageNumber > totalPages) return; - setCurrentPage(pageNumber); - }; - - const handleTabChange = (tab: "today" | "this-week" | "earlier") => { - setActiveTab(tab); - setCurrentPage(1); - }; - - const renderPageNumbers = () => { - if (totalPages <= 1) return null; - - return ( - <> - {displayedPages[0] > 1 && ( - <> - - {displayedPages[0] > 2 && ( - ... - )} - - )} - - {displayedPages.map((number) => ( - - ))} - - {displayedPages[displayedPages.length - 1] < totalPages && ( - <> - {displayedPages[displayedPages.length - 1] < totalPages - 1 && ( - ... - )} - - - )} - - ); - }; - - if (!notifications) { - return ( -
-
-
Loading notifications...
-
-
- ); - } - - if (error) { - return ( -
-
-
{error}
-
-
- ); - } - - return ( -
-
-

Notifications

-
- - -
-
- {/* Tabs */} -
- {(["today", "this-week", "earlier"] as const).map((tab) => ( - - ))} -
- {/* Notification List */} -
- {currentItems.length > 0 ? ( - currentItems.map((notif) => ( - - )) - ) : ( - - -

No notifications

-

You're all caught up!

-
- )} -
- {/* Pagination */} - {filteredNotifications.length > 0 && ( -
-
- Showing {indexOfFirstItem + 1} to{" "} - {Math.min(indexOfLastItem, filteredNotifications.length)} of{" "} - {filteredNotifications.length} results -
-
- - - {renderPageNumbers()} - - -
-
- )} - {content && ( - - )}{" "} -
- ); -} diff --git a/components/dashboard/tabs/payment-split.tsx b/components/dashboard/tabs/payment-split.tsx deleted file mode 100644 index 364d7e1..0000000 --- a/components/dashboard/tabs/payment-split.tsx +++ /dev/null @@ -1,222 +0,0 @@ -"use client"; - -import AddSplit from "@/components/modals/add-split"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/ui/cards"; -import { Button } from "@/components/ui/buttons"; -import { Loader2, Plus, Users } from "lucide-react"; -import React, { useCallback, useState, useEffect } from "react"; -import { useSplitPayments } from "@/components/hooks/useSplitPayments"; -import { TransactionPinDialog } from "@/components/ui/transaction-pin-dialog"; - -export default function PaymentSplit() { - const { - templates, - isLoading, - refetch, - executeSplitPayment, - toggleSplitPayment, - } = useSplitPayments(); - - const [addSplitModal, setAddSplitModal] = useState(false); - const [showPinDialog, setShowPinDialog] = useState(false); // Add PIN dialog state - const [selectedTemplateId, setSelectedTemplateId] = useState(null); // Track which template to execute - const [isExecuting, setIsExecuting] = useState(false); // Track execution loading state - - const refetchTemplates = useCallback(async () => { - await refetch(); - }, [refetch]); - - // Handle PIN confirmation for split payment execution - const handleExecuteWithPin = async (pin: string) => { - if (!selectedTemplateId) return; - - setIsExecuting(true); - try { - await executeSplitPayment(selectedTemplateId, pin); // Pass PIN to execute function - // Hook should automatically refresh templates after execution - } catch (error) { - console.error("Failed to execute:", error); - } finally { - setIsExecuting(false); - setShowPinDialog(false); - setSelectedTemplateId(null); - } - }; - - // Modified handleExecute to show PIN dialog - const handleExecute = async (id: string) => { - setSelectedTemplateId(id); - setShowPinDialog(true); - }; - - const handleToggle = async (id: string) => { - try { - await toggleSplitPayment(id); - // Hook automatically refreshes templates after toggle - } catch (error) { - console.error("Failed to toggle:", error); - } - }; - - const handleShowSplitModal = () => { - setAddSplitModal(true); - }; - - // Handle PIN dialog close - const handlePinDialogClose = () => { - setShowPinDialog(false); - setSelectedTemplateId(null); - }; - - return ( -
-
-

Split Payments

- -
- - {isLoading ? ( -
- -
- ) : templates.length === 0 ? ( - - -

- No split created yet. Create your first payment split to get - started. -

-
- ) : ( -
- {templates.map((template) => ( - - - {template.title} - - -

{template.description}

-
-
- Chain: - {template.chain.toUpperCase()} -
-
- Total Amount: - {template.totalAmount} -
-
- Recipients: - {template.recipientCount} -
-
- Executions: - {template.executionCount} -
-
- Status: - {template.status} -
-
-
- {template.canExecute && ( - - )} - -
-
-
- ))} -
- )} - -
- -

- How It Works -

-
-
-
- 1 -
-
-

Create Split

-

- Add multiple recipients with their wallet addresses and - amounts -

-
-
-
-
- 2 -
-
-

Save Template

-

- Create reusable template on the backend -

-
-
-
-
- 3 -
-
-

- Execute Payments -

-

- Distribute funds to all recipients in one go (PIN required) -

-
-
-
-
-
- - {addSplitModal && ( - - )} - - {/* PIN Dialog for Split Payment Execution */} - -
- ); -} \ No newline at end of file diff --git a/components/dashboard/tabs/profile.tsx b/components/dashboard/tabs/profile.tsx deleted file mode 100644 index 51dae77..0000000 --- a/components/dashboard/tabs/profile.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import EditProfile from "@/components/modals/edit-profile"; -import React from "react"; - -export default function Profile() { - return
- -
; -} diff --git a/components/dashboard/tabs/qr-payment.tsx b/components/dashboard/tabs/qr-payment.tsx deleted file mode 100644 index 0d041ca..0000000 --- a/components/dashboard/tabs/qr-payment.tsx +++ /dev/null @@ -1,541 +0,0 @@ -"use client"; - -import { Card } from "@/components/ui/Card"; -import { ChevronDown } from "lucide-react"; -import { useCallback, useMemo, useState, useEffect } from "react"; -import Image from "next/image"; -import QRCodeLib from "qrcode"; -import useExchangeRates from "@/components/hooks/useExchangeRate"; -import { QRCodeDisplay } from "@/components/modals/qr-code-display"; -import { useMerchantPayments } from "@/components/hooks/useMerchantPayments"; // ADD -import { normalizeStarknetAddress } from "@/components/lib/utils"; -import { AddressDropdown } from "@/components/modals/addressDropDown"; -import { useWalletData } from "@/components/hooks/useWalletData"; - -const generateQRData = ( - chain: string, - address: string, - amount: string | null = null, - label: string | null = null -): string => { - switch (chain.toLowerCase()) { - case "bitcoin": - case "btc": - let bitcoinUri = `bitcoin:${address}`; - const bitcoinParams = []; - if (amount) bitcoinParams.push(`amount=${amount}`); - if (label) bitcoinParams.push(`label=${encodeURIComponent(label)}`); - if (bitcoinParams.length > 0) { - bitcoinUri += `?${bitcoinParams.join("&")}`; - } - return bitcoinUri; - - case "ethereum": - case "eth": - let ethereumUri = `ethereum:${address}`; - const ethereumParams = []; - if (amount) { - const weiAmount = (parseFloat(amount) * Math.pow(10, 18)).toString(); - ethereumParams.push(`value=${weiAmount}`); - } - if (label) ethereumParams.push(`label=${encodeURIComponent(label)}`); - if (ethereumParams.length > 0) { - ethereumUri += `?${ethereumParams.join("&")}`; - } - return ethereumUri; - - case "solana": - case "sol": - let solanaUri = `solana:${address}`; - const solanaParams = []; - if (amount) solanaParams.push(`amount=${amount}`); - if (label) solanaParams.push(`label=${encodeURIComponent(label)}`); - if (solanaParams.length > 0) { - solanaUri += `?${solanaParams.join("&")}`; - } - return solanaUri; - - case "starknet": - case "strk": - let starknetUri = `starknet:${address}`; - const starknetParams = []; - if (amount) { - const weiAmount = (parseFloat(amount) * Math.pow(10, 18)).toString(); - starknetParams.push(`value=${weiAmount}`); - } - if (label) starknetParams.push(`label=${encodeURIComponent(label)}`); - if (starknetParams.length > 0) { - starknetUri += `?${starknetParams.join("&")}`; - } - return starknetUri; - - case "stellar": - case "xlm": - let stellarUri = `web+stellar:pay?destination=${address}`; - const stellarParams = []; - if (amount) { - // Stellar amounts are in lumens (1 XLM = 1,000,000 stroops) - // For QR codes, we typically use the base unit (lumens) - stellarParams.push(`amount=${amount}`); - } - if (label) stellarParams.push(`memo=${encodeURIComponent(label)}`); - if (stellarParams.length > 0) { - stellarUri += `&${stellarParams.join("&")}`; - } - return stellarUri; - - case "polkadot": - case "dot": - let polkadotUri = `substrate:${address}`; - const polkadotParams = []; - if (amount) { - // Polkadot amounts are in Planck (1 DOT = 10,000,000,000 Planck) - // For QR codes, we typically use the base unit (DOT) - polkadotParams.push(`amount=${amount}`); - } - if (label) polkadotParams.push(`label=${encodeURIComponent(label)}`); - if (polkadotParams.length > 0) { - polkadotUri += `?${polkadotParams.join("&")}`; - } - return polkadotUri; - - case "usdt_erc20": - return `ethereum:${address}`; - case "usdt_trc20": - return `tron:${address}`; - - default: - return address; - } -}; - -// Enhanced QR code generation function -const generateCompatibleQRCode = async ( - chain: string, - address: string, - options: { - amount?: string | null; - label?: string | null; - width?: number; - margin?: number; - darkColor?: string; - lightColor?: string; - errorCorrectionLevel?: "L" | "M" | "Q" | "H"; - } = {} -) => { - const { - amount = null, - label = null, - width = 200, - margin = 2, - darkColor = "#000000", - lightColor = "#FFFFFF", - errorCorrectionLevel = "M", - } = options; - - try { - const qrData = generateQRData(chain, address, amount, label); - const qrCodeDataUrl = await QRCodeLib.toDataURL(qrData, { - width, - margin, - errorCorrectionLevel, - type: "image/png" as "image/png" | "image/jpeg" | "image/webp", - color: { - dark: darkColor, - light: lightColor, - }, - }); - - return { - dataUrl: qrCodeDataUrl, - rawData: qrData, - format: getQRFormat(chain), - }; - } catch (error) { - console.error("Error generating compatible QR code:", error); - throw error; - } -}; - -// Helper function to get the format description -const getQRFormat = (chain: string): string => { - switch (chain.toLowerCase()) { - case "bitcoin": - case "btc": - return "BIP21 Bitcoin URI"; - case "ethereum": - case "eth": - return "EIP681 Ethereum URI"; - case "solana": - case "sol": - return "Solana URI Scheme"; - case "starknet": - case "strk": - return "Ethereum-compatible URI"; - case "stellar": - case "xlm": - return "Stellar URI Scheme"; - case "polkadot": - case "dot": - return "Polkadot URI Scheme"; - case "usdt_erc20": - return "ERC-20 Token URI"; - case "usdt_trc20": - return "TRC-20 Token URI"; - default: - return "Plain Address"; - } -}; -export default function QrPayment() { - const [token, setToken] = useState("STARKNET"); - const [amount, setAmount] = useState(""); - const [customerEmail, setCustomerEmail] = useState(""); - const [description, setDescription] = useState(""); - const [showQR, setShowQR] = useState(false); - const [qrData, setQrData] = useState(""); - const [isProcessing, setIsProcessing] = useState(false); - const [paymentId, setPaymentId] = useState(""); - const [localError, setLocalError] = useState(null); - const [paymentStatus, setPaymentStatus] = useState(""); - const { addresses } = useWalletData(); - - const tokenRate = (token: string) => { - if (token === "ETHEREUM") return "ETH"; - if (token === "BITCOIN") return "BTC"; - if (token === "SOLANA") return "SOL"; - if (token === "STARKNET") return "STRK"; - if (token === "USDT_TRC20") return "USDT"; - if (token === "USDT_ERC20") return "USDT"; - if (token === "POLKADOT") return "DOT"; - if (token === "STELLAR") return "XML"; - return "USDT"; - }; - const { rates, isLoading: ratesLoading } = useExchangeRates(); - const { createPayment, isLoading: merchantLoading } = useMerchantPayments(); - - const getTokenChain = useCallback((): string => { - const chainMap: { [key: string]: string } = { - ETHEREUM: "ethereum", - BITCOIN: "bitcoin", - SOLANA: "solana", - STARKNET: "starknet", - USDT_ERC20: "usdt_erc20", - USDT_TRC20: "usdt_trc20", - POLKADOT: "polkadot", - STELLAR: "stellar", - }; - return chainMap[token] || "ethereum"; - }, [token]); - - const singleAddress = addresses.filter( - (a) => a.chain === token.toLowerCase() - ); - - const currentReceiverAddress = useMemo((): string => { - if (!addresses || addresses.length === 0) return ""; - - const chain = getTokenChain(); - const addr = addresses.find((a) => a.chain === chain); - if (!addr) return ""; - - return normalizeStarknetAddress(addr.address, chain); - }, [addresses, getTokenChain]); - - const calculateTokenAmount = useCallback((): string => { - const ngnAmount = parseFloat(amount) || 0; - const rateKey = tokenRate(token); - const rate = rateKey ? rates[rateKey] : 1; - - if (!rate || rate === 0) { - console.log("Using fallback rate for calculation"); - return (ngnAmount / 1500).toFixed(6); - } - - const tokenAmount = ngnAmount / rate; - - return tokenAmount.toFixed(6); - }, [amount, rates, token]); - - const handleTokenSelect = (tkn: string) => { - setToken(tkn.toUpperCase()); - }; - - const handleCreatePaymentRequest = async () => { - if (!amount || !currentReceiverAddress) { - setLocalError( - "Please enter an amount and ensure wallet address is available" - ); - return; - } - - setIsProcessing(true); - setLocalError(null); - - try { - const tokenAmount = calculateTokenAmount(); - const chain = getTokenChain(); - - const qrResult = await generateCompatibleQRCode( - chain, - currentReceiverAddress, - { - amount: tokenAmount, - width: 200, - margin: 2, - errorCorrectionLevel: "M", - } - ); - - const requestBody: any = { - amount: parseFloat(tokenAmount), - chain: chain, - network: "mainnet", - description: description || "QRPayment request", - }; - - switch (chain.toLowerCase()) { - case "bitcoin": - requestBody.btcAddress = currentReceiverAddress; - break; - case "ethereum": - requestBody.ethAddress = currentReceiverAddress; - break; - case "solana": - requestBody.solAddress = currentReceiverAddress; - break; - case "starknet": - requestBody.strkAddress = currentReceiverAddress; - break; - case "usdt_erc20": - requestBody.usdtErc20Address = currentReceiverAddress; - break; - case "usdt_trc20": - requestBody.usdtTrc20Address = currentReceiverAddress; - break; - case "polkadot": - requestBody.dotAddress = currentReceiverAddress; - break; - case "stellar": - requestBody.xmlAddress = currentReceiverAddress; - break; - default: - requestBody.address = currentReceiverAddress; - } - - const response = await createPayment(requestBody); - - - if (response && response.payment) { - setPaymentId(response.payment.id || ""); - setPaymentStatus(response.payment.status); - setQrData(qrResult.dataUrl); - setShowQR(true); - } else { - throw new Error("Invalid response from server - no payment data"); - } - } catch (error: any) { - console.error("Error creating payment request:", error); - setLocalError(error.message || "Failed to create payment request"); - } finally { - setIsProcessing(false); - } - }; - - const handleCloseQR = () => { - setShowQR(false); - setQrData(""); - setPaymentId(""); - setLocalError(null); - setAmount(""); - }; - - const steps = [ - { - step: "Enter Amount", - description: "Specify the amount in NGN you want to receive", - }, - { - step: "Generate QR", - description: "Create a unique payment request QR code", - }, - { - step: "Share QR", - description: "Send the QR code or address to the payer", - }, - { - step: "Receive Payment", - description: "Funds will be credited after confirmation", - }, - ]; - - if (addresses.length === 0) { - return ( -
-
Loading wallet data...
-
- ); - } - - return ( -
-
- -

- Create Payment Request -

- -
-
- {/* Token Selection */} - - {/* Amount Input */} -
- - setAmount(e.target.value)} - placeholder="Enter amount in NGN" - className="w-full p-3 rounded-lg bg-background border border-border placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors" - min="0" - step="any" - disabled={ - merchantLoading || - ratesLoading || - isProcessing - } - /> -

- ≈ {calculateTokenAmount()} {token} -

-
-
- - setDescription(e.target.value)} - placeholder="Describe the payment purpose" - className="w-full p-3 rounded-lg bg-background border border-border placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors" - min="0" - step="any" - disabled={ - merchantLoading || - ratesLoading || - isProcessing - } - /> -
-
- - setCustomerEmail(e.target.value)} - placeholder="Enter customer email" - className="w-full p-3 rounded-lg bg-background border border-border placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors" - min="0" - step="any" - disabled={ - merchantLoading || - ratesLoading || - isProcessing - } - /> -
-
- - {/* Error Message */} - {localError && !showQR && ( -
-

{localError}

-
- )} - - {/* Generate Button */} - -
-
- - {/* Instructions */} - -

- How to Accept Payments -

-
- {steps.map((step, id) => ( -
-
- - {id + 1} - -
-
-

{step.step}

-

- {step.description} -

-
-
- ))} -
-
-
- - {/* QR Code Modal */} - {showQR && ( - - )} -
- ); -} diff --git a/components/dashboard/tabs/send-funds.tsx b/components/dashboard/tabs/send-funds.tsx deleted file mode 100644 index c60f55d..0000000 --- a/components/dashboard/tabs/send-funds.tsx +++ /dev/null @@ -1,589 +0,0 @@ -"use client"; - -import { Card } from "@/components/ui/Card"; -import { - Loader2, - ArrowUpRight, - Check, - TriangleAlert, - Copy, - CheckCheck, - AlertCircle, -} from "lucide-react"; -import React, { useState, useCallback, useEffect, useMemo } from "react"; -import { useAuth } from "@/components/context/AuthContext"; -import { shortenAddress } from "@/components/lib/utils"; -import useExchangeRates from "@/components/hooks/useExchangeRate"; -import { AddressDropdown } from "@/components/modals/addressDropDown"; -import { useWalletData } from "@/components/hooks/useWalletData"; -import { useTokenBalance } from "@/components/hooks"; -import { TransactionPinDialog } from "@/components/ui/transaction-pin-dialog"; - - -interface TokenOption { - symbol: string; - name: string; - chain: string; - network: string; - address: string; - hasWallet: boolean; -} - -export default function SendFunds() { - const [selectedToken, setSelectedToken] = useState("ethereum"); - const [showTokenDropdown, setShowTokenDropdown] = useState(false); - const [toAddress, setToAddress] = useState(""); - const [amount, setAmount] = useState(""); - const [copied, setCopied] = useState(false); - const [isSending, setIsSending] = useState(false); - const [showPinDialog, setShowPinDialog] = useState(false); - const [txStatus, setTxStatus] = useState<{ - type: "success" | "error" | null; - message: string; - txHash?: string; - }>({ type: null, message: "" }); - - const { sendTransaction } = useAuth(); - const { rates } = useExchangeRates(); - const { - addresses, - balances, - breakdown, - } = useWalletData(); - const { getTokenSymbol, getTokenName } = useTokenBalance(); - - // Token options based on available addresses - const tokenOptions: TokenOption[] = useMemo(() => { - if (!addresses) return []; - - return addresses.map((addr) => { - const balanceInfo = balances.find((b) => b.chain === addr.chain); - return { - symbol: getTokenSymbol(addr.chain), - name: getTokenName(addr.chain), - chain: addr.chain, - network: addr.network, - address: addr.address, - hasWallet: true, - balance: parseFloat(balanceInfo?.balance || "0"), - }; - }); - }, [addresses, balances]); // ADD balances dependency - // Get token symbol - - // Normalize and validate Starknet address - const normalizeStarknetAddress = (address: string): string => { - // Remove whitespace - let normalized = address.trim(); - - // Add 0x prefix if missing - if (!normalized.startsWith("0x")) { - normalized = "0x" + normalized; - } - - // Remove 0x for validation and padding - const hexPart = normalized.slice(2); - - // Validate hex characters only - if (!/^[0-9a-fA-F]*$/.test(hexPart)) { - throw new Error( - "Address contains invalid characters. Only hexadecimal characters (0-9, a-f, A-F) are allowed." - ); - } - - // Pad to 64 characters (without 0x) - const paddedHex = hexPart.padStart(64, "0"); - - // Check if address is too long after padding - if (paddedHex.length > 64) { - throw new Error( - "Address is too long. Maximum length is 66 characters (including 0x prefix)." - ); - } - - return "0x" + paddedHex; - }; - - const selectedTokenData = tokenOptions.find( - (token) => token.chain === selectedToken - ); - - // Get current wallet balance for selected token - const currentWalletBalance = useMemo(() => { - const balanceInfo = balances.find((b) => b.chain === selectedToken); - return parseFloat(balanceInfo?.balance || "0"); - }, [balances, selectedToken]); - // Get current wallet address and network for selected token - const currentWalletAddress = useMemo(() => { - if (!addresses) return ""; - const addressInfo = addresses.find((addr) => addr.chain === selectedToken); - return addressInfo?.address || ""; - }, [addresses, selectedToken]); - - const currentNetwork = useMemo(() => { - if (!addresses) return "testnet"; - const addressInfo = addresses.find((addr) => addr.chain === selectedToken); - return addressInfo?.network || "testnet"; - }, [addresses, selectedToken]); - - // Check if selected token has a wallet - const hasWalletForSelectedToken = useMemo(() => { - return !!currentWalletAddress; - }, [currentWalletAddress]); - - // Calculate NGN equivalent - const ngnEquivalent = useMemo(() => { - if (!amount || isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) - return 0; - - const tokenSymbol = getTokenSymbol(selectedToken); - const tokenRate = rates[tokenSymbol as keyof typeof rates] || 1; - return parseFloat(amount) * tokenRate; - }, [amount, selectedToken, rates]); - - // Validation - const validationError = useMemo(() => { - if (!hasWalletForSelectedToken) { - return "No wallet found for this currency"; - } - if (!toAddress.trim()) { - return "Recipient address is required"; - } - if (!amount || parseFloat(amount) <= 0) { - return "Amount must be greater than 0"; - } - if (parseFloat(amount) > currentWalletBalance) { - return "Insufficient balance"; - } - return null; - }, [hasWalletForSelectedToken, toAddress, amount, currentWalletBalance]); - - // Reset form - const resetForm = useCallback(() => { - setToAddress(""); - setAmount(""); - setTxStatus({ type: null, message: "" }); - }, []); - - // Handle token selection - const handleTokenSelect = useCallback( - (chain: string) => { - setSelectedToken(chain); - setShowTokenDropdown(false); - resetForm(); - }, - [resetForm] - ); - - // Copy address to clipboard - const handleCopyAddress = useCallback(async () => { - if (!currentWalletAddress) return; - - try { - await navigator.clipboard.writeText(currentWalletAddress); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (err) { - console.error("Failed to copy address: ", err); - } - }, [currentWalletAddress]); - - // Handle send transaction - const handleSendWithPin = async (pin: string) => { - if (validationError) { - setTxStatus({ - type: "error", - message: validationError, - }); - setShowPinDialog(false); - return; - } - - setIsSending(true); - setTxStatus({ type: null, message: "" }); - - try { - let normalizedToAddress = toAddress.trim(); - let normalizedFromAddress = currentWalletAddress.trim(); - - // Special handling for Starknet addresses - if (selectedToken === "starknet") { - try { - normalizedToAddress = normalizeStarknetAddress(toAddress); - normalizedFromAddress = - normalizeStarknetAddress(currentWalletAddress); - console.log("Normalized Starknet address:", normalizedToAddress); - } catch (error) { - throw new Error( - error instanceof Error - ? `Invalid Starknet address: ${error.message}` - : "Invalid Starknet address format" - ); - } - } - - - // Include PIN in the transaction payload - const response = await sendTransaction({ - chain: selectedToken, - network: currentNetwork, - toAddress: normalizedToAddress, - amount: amount, - fromAddress: currentWalletAddress, - transactionPin: pin, - }); - - setTxStatus({ - type: "success", - message: "Transaction sent successfully!", - txHash: response.txHash, - }); - - // Close PIN dialog - setShowPinDialog(false); - - // Reset form after 10 seconds - setTimeout(() => { - resetForm(); - }, 10000); - } catch (error: any) { - console.error("Transaction error:", error); - - let errorMessage = "Failed to send transaction. Please try again."; - - if (error.message) { - errorMessage = error.message; - } else if (typeof error === "string") { - errorMessage = error; - } - - setTxStatus({ - type: "error", - message: errorMessage, - }); - - // Close PIN dialog on error - setShowPinDialog(false); - } finally { - setIsSending(false); - } - }; - - // Modified handleSendTransaction to show PIN dialog - const handleSendTransaction = () => { - if (validationError) { - setTxStatus({ - type: "error", - message: validationError, - }); - return; - } - - // Show PIN dialog instead of immediately sending - setShowPinDialog(true); - }; - - // Handle PIN dialog close - const handlePinDialogClose = () => { - setShowPinDialog(false); - }; - - - // Close dropdown when clicking outside - useEffect(() => { - const handleClickOutside = () => { - if (showTokenDropdown) { - setShowTokenDropdown(false); - } - }; - - document.addEventListener("click", handleClickOutside); - return () => { - document.removeEventListener("click", handleClickOutside); - }; - }, [showTokenDropdown]); - - // Format balance display - const formatBalance = (balance: number): string => { - if (balance === 0) return "0.00"; - if (balance < 0.001) return "<0.001"; - return balance.toFixed(4); - }; - - // Format NGN currency - const formatNGN = (amount: number): string => { - return new Intl.NumberFormat("en-NG", { - style: "currency", - currency: "NGN", - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }).format(amount); - }; - - // Get block explorer URL - const getExplorerUrl = (txHash: string): string => { - const explorerUrls: { - [key: string]: { testnet: string; mainnet: string }; - } = { - ethereum: { - testnet: `https://sepolia.etherscan.io/tx/${txHash}`, - mainnet: `https://etherscan.io/tx/${txHash}`, - }, - usdt_erc20: { - testnet: `https://sepolia.etherscan.io/tx/${txHash}`, - mainnet: `https://etherscan.io/tx/${txHash}`, - }, - bitcoin: { - testnet: `https://blockstream.info/testnet/tx/${txHash}`, - mainnet: `https://blockstream.info/tx/${txHash}`, - }, - solana: { - testnet: `https://explorer.solana.com/tx/${txHash}?cluster=devnet`, - mainnet: `https://explorer.solana.com/tx/${txHash}`, - }, - starknet: { - testnet: `https://sepolia.voyager.online/tx/${txHash}`, - mainnet: `https://voyager.online/tx/${txHash}`, - }, - }; - - const explorer = explorerUrls[selectedToken]; - if (!explorer) return "#"; - - return currentNetwork === "testnet" ? explorer.testnet : explorer.mainnet; - - }; - - - - if (addresses.length === 0) { - return ( -
-
- -

Loading wallet data...

-
-
- ); - } - - if (!addresses || addresses.length === 0) { - return ( -
- -

- No Wallets Available -

-

- No Velo wallets found. Please create wallets first to send funds. -

-
-
- ); - } - - return ( -
-
- - {/* Header */} -
-

Send Payment

-

- Transfer funds from your Velo wallet to any valid address -

-
- -
- {/* Transaction Status */} - {txStatus.type && ( -
-
- {txStatus.type === "success" ? ( - - ) : ( - - )} - - {txStatus.type === "success" ? "Success" : "Error"} - -
-

- {txStatus.message} -

- {txStatus.txHash && ( - - View on Explorer - - - )} -
- )} - - {/* Wallet Status Warning */} - {!hasWalletForSelectedToken && ( -
-
- - No Wallet Found -
-

- No Velo wallet found for {getTokenName(selectedToken)}. You can - only send from wallets created in Velo. -

-
- )} - -
-
- - - {/* Selected Wallet Address */} - {currentWalletAddress && ( -
-
- From Address: - -
-

{shortenAddress(currentWalletAddress, 8)}

-

Network: {currentNetwork}

-
- )} -
- -
- {/* Recipient Address */} -
- - setToAddress(e.target.value)} - placeholder={`Enter ${selectedTokenData?.name || selectedToken} address`} - className="w-full p-3 rounded-lg bg-background border border-border placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors font-mono text-sm" - disabled={!hasWalletForSelectedToken || isSending} - /> - {selectedToken === "starknet" && toAddress && ( -

Tip: Address will be automatically formatted with 0x prefix and proper padding

- )} -
- - {/* Amount */} -
- -
- { - const value = e.target.value; - if (/^\d*\.?\d*$/.test(value)) { - setAmount(value); - } - }} - placeholder="0.00" - className="w-full p-3 rounded-lg bg-background border border-border placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors pr-20 disabled:opacity-50" - disabled={!hasWalletForSelectedToken || isSending} - /> -
{selectedTokenData?.symbol}
-
-
- Available: {formatBalance(currentWalletBalance)} {selectedTokenData?.symbol} - {ngnEquivalent > 0 && ≈ {formatNGN(ngnEquivalent)}} -
-
-
-
- - {/* Send Button */} -
- -
- - {/* Network Info */} -
-

Sending on {currentNetwork} network

-
-
- - {/* Instructions */} -
- - -

Important Notes

-
    -
  • Recipient does NOT need to be a Velo user
  • -
  • Only send to valid addresses for the selected currency
  • -
  • Transactions are irreversible once confirmed
  • -
  • Double-check addresses before sending
  • - {selectedToken === "starknet" && ( - <> -
  • - Starknet wallets may need deployment (auto-handled) -
  • -
  • - Addresses will be auto-formatted with 0x prefix and padding -
  • - - )} -
-
- - -
-
- ); -} diff --git a/components/dashboard/tabs/services.tsx b/components/dashboard/tabs/services.tsx deleted file mode 100644 index 88b8d0c..0000000 --- a/components/dashboard/tabs/services.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React, { useState } from "react"; -import { PhoneCall, Wifi, Tv, Search, Zap, Lightbulb } from "lucide-react"; -import Airtime from "./airtime"; -import Data from "./data"; -import Electricity from "./electricity"; - -const TABS = [ - { key: "Airtime", label: "Airtime", icon: PhoneCall }, - { key: "Data", label: "Data", icon: Wifi }, - { key: "Electricity", label: "Electricity", icon: Lightbulb }, -]; - -export default function Services() { - const [activeTab, setActiveTab] = useState("Airtime"); - const ActiveIcon = TABS.find((t) => t.key === activeTab)?.icon || PhoneCall; - - return ( -
-
- {/* Left info / quick actions - */} - - {/* Main content */} -
-
-
-
- -
-
-

{activeTab}

-

- {activeTab === "Airtime" - ? "Quick airtime purchases" - : activeTab === "Data" - ? "Flexible data plans" - : "Cable subscriptions"} -

-
-
- -
- Step -
- 1 of 3 -
-
-
- -
-
- {TABS.map((tab) => ( - - ))} -
-
- -
- {activeTab === "Airtime" && } - {activeTab === "Data" && } - {activeTab === "Electricity" && } -
-
-
-
- ); -} diff --git a/components/dashboard/tabs/swap.tsx b/components/dashboard/tabs/swap.tsx deleted file mode 100644 index c89dd34..0000000 --- a/components/dashboard/tabs/swap.tsx +++ /dev/null @@ -1,463 +0,0 @@ -"use client"; - -import { Card } from "@/components/ui/Card"; -import { ChevronDown, Shuffle } from "lucide-react"; -import { useCallback, useState } from "react"; -import useExchangeRates from "@/components/hooks/useExchangeRate"; -import Image from "next/image"; - -export default function Swap() { - const [fromToken, setFromToken] = useState("USDT"); - const [toToken, setToToken] = useState("NGN"); - const [fromAmount, setFromAmount] = useState(""); - const [toAmount, setToAmount] = useState(""); - const [showFromDropdown, setShowFromDropdown] = useState(false); - const [showToDropdown, setShowToDropdown] = useState(false); - const [showConfirmModal, setShowConfirmModal] = useState(false); - const [showSuccessModal, setShowSuccessModal] = useState(false); - const [isProcessing, setIsProcessing] = useState(false); - - const { rates } = useExchangeRates(); - - const tokens = [ - { symbol: "USDT", name: "Tether", icon: "/usdtlogo.svg" }, - { symbol: "USDC", name: "USD Coin", icon: "🔵" }, - { symbol: "STRK", name: "Starknet", icon: "⭐" }, - { symbol: "ETH", name: "Ethereum", icon: "💎" }, - ]; - - const calculateExchange = useCallback( - (amount: string, from: string, to: string) => { - if (!amount || !rates[from as keyof typeof rates] || !rates[to as keyof typeof rates]) - return ""; - - const fromRate = rates[from as keyof typeof rates] || 0; - const toRate = rates[to as keyof typeof rates] || 1; - - if (from === "NGN") { - return (parseFloat(amount) / fromRate).toFixed(6); - } else if (to === "NGN") { - return (parseFloat(amount) * fromRate).toFixed(2); - } else { - return ((parseFloat(amount) * fromRate) / toRate).toFixed(6); - } - }, - [rates] - ); - - const handleFromAmountChange = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - if (/^\d*\.?\d*$/.test(value)) { - setFromAmount(value); - setToAmount(calculateExchange(value, fromToken, toToken)); - } - }, - [fromToken, toToken, calculateExchange] - ); - - const handleToAmountChange = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - if (/^\d*\.?\d*$/.test(value)) { - setToAmount(value); - setFromAmount(calculateExchange(value, toToken, fromToken)); - } - }, - [fromToken, toToken, calculateExchange] - ); - - const handleSwapTokens = useCallback(() => { - const tempFromToken = fromToken; - const tempFromAmount = fromAmount; - - setFromToken(toToken); - setToToken(tempFromToken); - setFromAmount(toAmount); - setToAmount(tempFromAmount); - }, [fromToken, toToken, fromAmount, toAmount]); - - const handleSwap = useCallback(() => { - if (!fromAmount) return; - setShowConfirmModal(true); - }, [fromAmount]); - - const confirmSwap = useCallback(async () => { - setShowConfirmModal(false); - setIsProcessing(true); - - setTimeout(() => { - setIsProcessing(false); - setShowSuccessModal(true); - setFromAmount(""); - setToAmount(""); - }, 3000); - }, []); - - const fees = fromAmount ? ( - - {(parseFloat(fromAmount) * 0.005).toFixed(6)} - - ) : ( - 0.0 - ); - - const receiveAmount = toAmount ? ( - - {( - parseFloat(toAmount) - - parseFloat( - fromAmount ? (parseFloat(fromAmount) * 0.005).toString() : "0" - ) - ).toFixed(2)} - - ) : ( - -- - ); - - return ( -
- -
-
-

Swap

-

- Exchange between different tokens and Naira -

-
- -
-
- -
-
- - From - -
-
-
-
- - - {showFromDropdown && ( -
- - {tokens - .filter((token) => token.symbol !== toToken) - .map((token) => ( - - ))} -
- )} -
-
- - -
-
-
- -
- -
- - -
- - To - -
-
-
- - - {showToDropdown && ( -
- - {tokens - .filter((token) => token.symbol !== fromToken) - .map((token) => ( - - ))} -
- )} -
-
- - -
-
-
-
- -
-
- - Fees (0.5%) - - {fees} {fromToken} -
-
- - You will receive - - - ~ {receiveAmount} {toToken === "NGN" ? "" : toToken} - -
-
- -
- -
-
-
-
- - {showConfirmModal && ( -
- -
-
-
-
- -
-

- Confirm Swap -

-

- Swap {fromAmount} {fromToken} for {receiveAmount} {toToken}? -

-
- -
- - -
-
-
-
- )} - - {showSuccessModal && ( -
- -
-
- - - -
- -
-

- Swap Successful! -

-

- You received {receiveAmount} {toToken} -

-
- - -
-
-
- )} -
- ); -} \ No newline at end of file diff --git a/components/dashboard/tabs/top-up.tsx b/components/dashboard/tabs/top-up.tsx deleted file mode 100644 index 0d9eace..0000000 --- a/components/dashboard/tabs/top-up.tsx +++ /dev/null @@ -1,143 +0,0 @@ -"use client"; - -import { useState } from "react"; -// Assuming Card component is styled like the one in the screenshot (dark, slightly translucent) -import { Card } from "@/components/ui/Card"; -import { ChevronLeft } from "lucide-react"; -// Components for each step (assuming they handle their own internal styling) -import TokenSelection from "@/components/modals/token-selection"; -import AmountEntry from "@/components/modals/amount-entry"; -import Confirmation from "@/components/modals/confirmation"; - -type TopUpStep = "selection" | "amount" | "confirmation"; - -const steps: TopUpStep[] = ["selection", "amount", "confirmation"]; -const stepTitles = { - selection: "Select Cryptocurrency", - amount: "Enter Amount", - confirmation: "Confirm Purchase", -}; - -export default function TopUp() { - const [currentStep, setCurrentStep] = useState("selection"); - const [selectedToken, setSelectedToken] = useState(""); - const [amountData, setAmountData] = useState({ - ngnAmount: "", - cryptoAmount: "", - }); - - const handleTokenSelect = (token: string) => { - setSelectedToken(token); - setCurrentStep("amount"); - }; - - const handleAmountSubmit = (ngnAmount: string, cryptoAmount: string) => { - setAmountData({ ngnAmount, cryptoAmount }); - setCurrentStep("confirmation"); - }; - - const handleBack = () => { - if (currentStep === "amount") { - setCurrentStep("selection"); - } else if (currentStep === "confirmation") { - setCurrentStep("amount"); - } - }; - - const handleComplete = () => { - // Reset flow after completion - setCurrentStep("selection"); - setSelectedToken(""); - setAmountData({ ngnAmount: "", cryptoAmount: "" }); - // In a real app, you might want to show a success message here before redirecting - }; - - const currentStepIndex = steps.indexOf(currentStep); - - return ( -
-
- {/* Adjusted Card: slightly wider for better flow, using a richer dark background */} - - {/* Header */} -
- {currentStep !== "selection" && ( - - )} -
- {/* Stronger, more prominent title */} -

- Buy Crypto -

- {/* Current Step Title/Subtitle */} -

- {stepTitles[currentStep]} -

-
-
- - {/* Progress Steps: Modern Pill/Bar Indicator */} -
- {steps.map((step, index) => ( -
- {/* Step Marker Pill */} -
- {/* Step Label (Hidden on small screens, optional) */} - - {stepTitles[step]} - -
- ))} -
- - {/* Step Content */} - {/* The min-h is kept to prevent layout shift */} -
- {currentStep === "selection" && ( - - )} - {currentStep === "amount" && ( - - )} - {currentStep === "confirmation" && ( - - )} -
- -
-
- ); -} \ No newline at end of file From 29dfcda502d852ba8d8c70e086714f10e0697ca1 Mon Sep 17 00:00:00 2001 From: tali-creator Date: Thu, 4 Dec 2025 04:17:16 +0100 Subject: [PATCH 8/9] refactored code base --- .../purchase/PurchaseCommon/DataPlanSelect.tsx | 17 +++++------------ components/hooks/usePurchaseFlow.ts | 9 +++++---- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/components/dashboard/purchase/PurchaseCommon/DataPlanSelect.tsx b/components/dashboard/purchase/PurchaseCommon/DataPlanSelect.tsx index e9360c2..b421365 100644 --- a/components/dashboard/purchase/PurchaseCommon/DataPlanSelect.tsx +++ b/components/dashboard/purchase/PurchaseCommon/DataPlanSelect.tsx @@ -57,9 +57,9 @@ export function DataPlanSelect({ }; return ( -
+
-
+
{dataPlans.map((plan) => (
-
+

{plan.name}

-

- {plan.validity} -

- {plan.description && ( -

- {plan.description} -

- )} -
+
{formatAmount(plan.amount)}
+
diff --git a/components/hooks/usePurchaseFlow.ts b/components/hooks/usePurchaseFlow.ts index 02ef68f..2c2efe0 100644 --- a/components/hooks/usePurchaseFlow.ts +++ b/components/hooks/usePurchaseFlow.ts @@ -6,6 +6,7 @@ import useExchangeRates from "@/components/hooks/useExchangeRate"; import { normalizeStarknetAddress } from "@/components/lib/utils"; import { apiClient } from "@/lib/api-client"; import { validatePhoneNumber } from "@/lib/utils"; +import { toast } from "sonner"; export interface PurchaseFormData { service_id: string; @@ -201,7 +202,7 @@ export function usePurchaseFlow({ type }: { type: "airtime" | "data" | "electric // Verify meter number const handleVerifyMeter = useCallback(async () => { if (!formData.customer_id || !formData.service_id) { - setMeterVerificationMessage("Please enter meter number and select provider"); + toast.error("Please enter meter number and select provider"); return; } @@ -219,14 +220,14 @@ export function usePurchaseFlow({ type }: { type: "airtime" | "data" | "electric const customerInfo = result.data.customerName ? `✓ ${result.data.customerName} ` : `✓ Meter verified: ${result.data.company}`; - setMeterVerificationMessage(customerInfo); + toast.success(customerInfo); } else { setMeterVerified(false); - setMeterVerificationMessage(result.message || "✗ Invalid meter number"); + toast.error(result.message || "✗ Invalid meter number"); } } catch (error: any) { setMeterVerified(false); - setMeterVerificationMessage( + toast.error( error.message || "Verification failed. Please try again." ); } finally { From 866b08e815a8ddc1c52abf5455d143ffee3dfd54 Mon Sep 17 00:00:00 2001 From: tali-creator Date: Thu, 4 Dec 2025 04:35:35 +0100 Subject: [PATCH 9/9] refactored code base --- app/api/transaction-monitor/route.ts | 58 - app/dashboard/page.tsx | 14 +- app/dashboard/test/page.tsx | 2 +- components/dashboard/recent-activity.tsx | 3 +- components/dashboard/service-flow.tsx | 1594 ---------------------- service/blockchain-manager.ts | 92 -- 6 files changed, 8 insertions(+), 1755 deletions(-) delete mode 100644 app/api/transaction-monitor/route.ts delete mode 100644 components/dashboard/service-flow.tsx delete mode 100644 service/blockchain-manager.ts diff --git a/app/api/transaction-monitor/route.ts b/app/api/transaction-monitor/route.ts deleted file mode 100644 index a23b62e..0000000 --- a/app/api/transaction-monitor/route.ts +++ /dev/null @@ -1,58 +0,0 @@ -// app/api/wallet-monitor/route.ts -import { NextRequest, NextResponse } from "next/server"; -import { BlockchainManager } from "@/service/blockchain-manager"; -const blockchainManager = new BlockchainManager(); - -interface MultiChainWalletMonitorRequest { - chain: string; - walletAddress: string; - fromBlock?: number; -} - -export async function POST(request: NextRequest) { - // ... authentication and rate limiting (same as before) - - try { - const body: Partial = await request.json(); - const { chain, walletAddress, fromBlock } = body; - - if (!chain || !walletAddress) { - return NextResponse.json( - { error: "Missing required parameters: chain, walletAddress" }, - { status: 400 } - ); - } - - const result = await blockchainManager.monitorWallet( - chain, - walletAddress, - fromBlock - ); - - return NextResponse.json(result); - } catch { - // Error handling - } -} - -// GET endpoint to list supported chains -export async function GET(request: NextRequest) { - const url = new URL(request.url); - const action = url.searchParams.get("action"); - - if (action === "chains") { - const chains = blockchainManager.getSupportedChains(); - return NextResponse.json({ chains }); - } - - if (action === "status") { - const status = await blockchainManager.testAllConnections(); - return NextResponse.json({ status }); - } - - // Existing monitoring functionality - // const chain = url.searchParams.get("chain"); - // const walletAddress = url.searchParams.get("wallet"); - // const fromBlockParam = url.searchParams.get("fromBlock"); - -} \ No newline at end of file diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 6f84cf9..2ea51ed 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -8,6 +8,7 @@ import { RecentActivity } from "@/components/dashboard/recent-activity"; import { WalletOverview } from "@/components/dashboard/wallet-overview"; import { useAuth } from "@/components/context/AuthContext"; import { useState } from "react"; +import Link from "next/link"; interface RecentActivity { id: string; @@ -19,11 +20,8 @@ interface RecentActivity { status: "completed" | "pending" | "failed"; } -export interface DashboardProps { - activeTab: React.Dispatch>; -} -export default function DashboardHome({ activeTab }: DashboardProps) { +export default function DashboardHome() { const { user } = useAuth(); const [hideBalalance, setHideBalance] = useState(false); @@ -66,7 +64,7 @@ export default function DashboardHome({ activeTab }: DashboardProps) {
- +
{/* Bottom CTA */} @@ -80,12 +78,12 @@ export default function DashboardHome({ activeTab }: DashboardProps) {
- +
diff --git a/app/dashboard/test/page.tsx b/app/dashboard/test/page.tsx index 76776a5..1b45fc4 100644 --- a/app/dashboard/test/page.tsx +++ b/app/dashboard/test/page.tsx @@ -4,7 +4,7 @@ import React from 'react' export default function TEst() { return (
- + {/* */}
) } diff --git a/components/dashboard/recent-activity.tsx b/components/dashboard/recent-activity.tsx index 98b2130..f213e44 100644 --- a/components/dashboard/recent-activity.tsx +++ b/components/dashboard/recent-activity.tsx @@ -5,12 +5,11 @@ import { CardTitle, } from "@/components/ui/cards"; import { ArrowDownLeft, ArrowUpRight, ChevronRight } from "lucide-react"; -import { DashboardProps } from "./tabs/dashboard"; import { useNotifications } from "../hooks/useNotifications"; import { shortenAddress } from "../lib/utils"; import Link from "next/link"; -export function RecentActivity({ activeTab }: DashboardProps) { +export function RecentActivity() { const { notifications } = useNotifications(); const filtered = notifications.filter((notif) => { diff --git a/components/dashboard/service-flow.tsx b/components/dashboard/service-flow.tsx deleted file mode 100644 index 842a514..0000000 --- a/components/dashboard/service-flow.tsx +++ /dev/null @@ -1,1594 +0,0 @@ -"use client"; -import { motion, AnimatePresence } from "framer-motion"; -import { - Check, - X, - PhoneCall, - Wifi, - Zap, - ArrowRight, - Loader2, - AlertCircle, - ArrowLeft, - ChevronRight, - Home, -} from "lucide-react"; -// import '@/styles/service-styles.css'; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { TransactionPinDialog } from "../ui/transaction-pin-dialog"; -import { normalizeStarknetAddress } from "../lib/utils"; -import { useAuth } from "../context/AuthContext"; -import { useWalletData } from "../hooks"; -import useExchangeRates from "../hooks/useExchangeRate"; -import { AddressDropdown } from "../modals/addressDropDown"; -import Image from "next/image"; -import { - apiClient, - type DataPlan, - type ElectricityCompany, - type MeterType, - type ExpectedAmount, - type SupportedNetwork, -} from "@/lib/api-client"; -import { validatePhoneNumber } from "@/lib/utils"; - -export enum Blockchain { - ETHEREUM = "ethereum", - BITCOIN = "bitcoin", - SOLANA = "solana", - STELLAR = "stellar", - POLKADOT = "polkadot", - STARKNET = "starknet", - USDT_ERC20 = "usdt-erc20", -} - -type PurchaseType = "airtime" | "data" | "electricity"; - -interface PurchaseProps { - type: PurchaseType; -} - -type TransactionData = { - dateTime: string; - paymentMethod: string; - status: string; - description: string; - transactionId: string; - providerLogo: string; - providerName: string; - planName: string; - meterToken?: string; -}; - -type Provider = { - serviceID: string; - name: string; - image: string; - code?: string; - minAmount?: number; - maxAmount?: number; -}; - -export default function Purchase({ type }: PurchaseProps) { - const [step, setStep] = useState(1); - const [loading, setLoading] = useState(false); - const [success, setSuccess] = useState(null); - const [showPinDialog, setShowPinDialog] = useState(false); - const [isSending, setIsSending] = useState(false); - const [selectedToken, setSelectedToken] = useState("ethereum"); - const [toAddress, setToAddress] = useState(""); - const [txHash, setTxHash] = useState(""); - const [errorMessage, setErrorMessage] = useState(""); - const [merchantFallback, setMerchantFallback] = useState(false); - const [verifyingMeter, setVerifyingMeter] = useState(false); - const [meterVerified, setMeterVerified] = useState(false); - const [meterVerificationMessage, setMeterVerificationMessage] = useState(""); - - const [formData, setFormData] = useState({ - service_id: "", - amount: "", - customer_id: "", - meterType: "prepaid" as "prepaid" | "postpaid", - dataplan: null as DataPlan | null, - expectedAmount: null as ExpectedAmount | null, - transactionData: null as TransactionData | null, - phoneNo: "", - }); - - const { sendTransaction } = useAuth(); - const { rates } = useExchangeRates(); - const { addresses, balances } = useWalletData(); - - const [providers, setProviders] = useState([]); - const [dataPlans, setDataPlans] = useState([]); - const [electricityCompanies, setElectricityCompanies] = useState< - ElectricityCompany[] - >([]); - const [meterTypes, setMeterTypes] = useState([]); - - const presetAmounts = [100, 200, 500, 1000, 2000, 5000]; - - const getConfig = () => { - const config = { - airtime: { - title: "Buy Airtime", - icon: PhoneCall, - step1Title: "Select network", - step1Description: "Choose your network provider", - customerLabel: "Phone Number", - placeholder: "8012345678", - showAmountGrid: true, - showVariations: false, - showMeterType: false, - showVerifyButton: false, - }, - data: { - title: "Purchase Data", - icon: Wifi, - step1Title: "Select network", - step1Description: "Choose your network provider", - customerLabel: "Phone Number", - placeholder: "8012345678", - showAmountGrid: false, - showVariations: true, - showMeterType: false, - showVerifyButton: false, - }, - electricity: { - title: "Electricity Bill", - icon: Zap, - step1Title: "Select provider", - step1Description: "Choose your electricity provider", - customerLabel: "Meter Number", - placeholder: "Enter meter number", - showAmountGrid: true, - showVariations: false, - showMeterType: true, - showVerifyButton: true, - }, - }; - return config[type]; - }; - - const getToAddress = (chain: string) => { - // Allow a runtime override for quick testing in the browser. Set - // `window.__VELO_MERCHANT_WALLETS = { ethereum: '0x...', solana: '...'} - // in DevTools to test without restarting the dev server. - if (typeof window !== "undefined") { - const runtime: any = (window as any).__VELO_MERCHANT_WALLETS; - if (runtime && typeof runtime === "object" && runtime[chain]) { - return String(runtime[chain]); - } - } - - const walletMap: { [key: string]: string | undefined } = { - ethereum: process.env.NEXT_PUBLIC_ETH_WALLET, - bitcoin: process.env.NEXT_PUBLIC_BTC_WALLET, - solana: process.env.NEXT_PUBLIC_SOL_WALLET, - stellar: process.env.NEXT_PUBLIC_XLM_WALLET, - polkadot: process.env.NEXT_PUBLIC_DOT_WALLET, - starknet: process.env.NEXT_PUBLIC_STRK_WALLET, - "usdt-erc20": process.env.NEXT_PUBLIC_USDT_WALLET, - }; - return walletMap[chain] || ""; - }; - - const config = getConfig(); - - const resetForm = useCallback(() => { - setToAddress(""); - setTxHash(""); - setErrorMessage(""); - setMeterVerified(false); - setMeterVerificationMessage(""); - }, []); - - const handleTokenSelect = useCallback((chain: string) => { - setSelectedToken(chain); - setToAddress(getToAddress(chain.toLowerCase())); - // resetForm(); - }, []); - - // Fetch providers based on type - const fetchProviders = async () => { - setLoading(true); - try { - if (type === "electricity") { - const { companies } = await apiClient.getElectricitySupportedOptions(); - setElectricityCompanies(companies); - - const mappedProviders: Provider[] = companies.map((company) => ({ - serviceID: company.value, - name: company.label, - image: `/img/${company.value}.png`, - code: company.code, - minAmount: company.minAmount, - maxAmount: company.maxAmount, - })); - setProviders(mappedProviders); - - // console.log("providers xxxxxxxxx ", mappedProviders); - } else { - // For airtime and data - const networks = - type === "data" - ? await apiClient.getDataSupportedNetworks() - : await apiClient.getAirtimeSupportedNetworks(); - - const mappedProviders: Provider[] = networks.map((network) => ({ - serviceID: network.value, - name: network.label, - image: `/img/${network.value.toLowerCase()}.png`, // Ensure lowercase - })); - setProviders(mappedProviders); - - // console.log("Airtime/Data providers loaded:", mappedProviders); - } - } catch (error) { - console.error("Failed to fetch providers:", error); - setErrorMessage("Failed to load providers. Please try again."); - } finally { - setLoading(false); - } - }; - - // Fetch data plans for selected network - const fetchDataPlans = async (network: string) => { - setLoading(true); - try { - const plans = await apiClient.getDataPlans(network, false); - setDataPlans(plans); - } catch (error) { - console.error("Failed to fetch data plans:", error); - setErrorMessage("Failed to load data plans. Please try again."); - } finally { - setLoading(false); - } - }; - - // Fetch meter types for electricity - const fetchMeterTypes = async () => { - setLoading(true); - try { - const { meterTypes: types } = - await apiClient.getElectricitySupportedOptions(); - setMeterTypes(types); - } catch (error) { - console.error("Failed to fetch meter types:", error); - } finally { - setLoading(false); - } - }; - - // Verify meter number - const handleVerifyMeter = async () => { - if (!formData.customer_id || !formData.service_id) { - setMeterVerificationMessage( - "Please enter meter number and select provider" - ); - return; - } - - setVerifyingMeter(true); - setMeterVerificationMessage(""); - - try { - const result = await apiClient.verifyElectricityMeter( - formData.service_id, - formData.customer_id - ); - - // console.log("results: ", result); - - // FIXED: Check the direct success property and data.valid - if (result.success && result.data && result.data.valid) { - setMeterVerified(true); - // console.log("meter", result.data); - - // Show customer name if available - const customerInfo = result.data.customerName - ? `✓ ${result.data.customerName} ` - : `✓ Meter verified: ${result.data.company}`; - - setMeterVerificationMessage(customerInfo); - } else { - setMeterVerified(false); - setMeterVerificationMessage(result.message || "✗ Invalid meter number"); - } - } catch (error: any) { - setMeterVerified(false); - setMeterVerificationMessage( - error.message || "Verification failed. Please try again." - ); - } finally { - setVerifyingMeter(false); - } - }; - - // Get expected crypto amount - const fetchExpectedAmount = async () => { - try { - let expectedAmount: ExpectedAmount; - - if (type === "airtime") { - const amount = parseFloat(formData.amount); - expectedAmount = await apiClient.getAirtimeExpectedAmount( - amount, - selectedToken - ); - } else if (type === "electricity") { - const amount = parseFloat(formData.amount); - expectedAmount = await apiClient.getElectricityExpectedAmount( - amount, - selectedToken - ); - } else if (type === "data" && formData.dataplan) { - expectedAmount = await apiClient.getDataExpectedAmount( - formData.dataplan.dataplanId, - formData.service_id, - selectedToken - ); - } else { - throw new Error("Invalid purchase configuration"); - } - - setFormData((prev) => ({ ...prev, expectedAmount })); - } catch (error: any) { - console.error("Failed to fetch expected amount:", error); - setErrorMessage(error.message || "Failed to calculate crypto amount"); - } - }; - - // console.log("calidate phone number", validatePhoneNumber("08101843464")); - - useEffect(() => { - fetchProviders(); - if (type === "electricity") { - fetchMeterTypes(); - } - }, [type]); - - useEffect(() => { - if (type === "data" && formData.service_id) { - fetchDataPlans(formData.service_id); - } - }, [type, formData.service_id]); - - useEffect(() => { - if (step === 2 && selectedToken) { - fetchExpectedAmount(); - } - }, [step, selectedToken, formData.amount, formData.dataplan]); - - const currentWalletBalance = useMemo(() => { - const balanceInfo = balances.find( - (b) => (b.chain || "").toLowerCase() === selectedToken.toLowerCase() - ); - return parseFloat(balanceInfo?.balance || "0"); - }, [balances, selectedToken]); - - const currentWalletAddress = useMemo(() => { - if (!addresses) return ""; - const addressInfo = addresses.find( - (addr) => (addr.chain || "").toLowerCase() === selectedToken.toLowerCase() - ); - return addressInfo?.address || ""; - }, [addresses, selectedToken]); - - // console.log("current wallet", currentWalletAddress); - const currentNetwork = useMemo(() => { - if (!addresses) return "mainnet"; - const addressInfo = addresses.find( - (addr) => (addr.chain || "").toLowerCase() === selectedToken.toLowerCase() - ); - return addressInfo?.network || "mainnet"; - }, [addresses, selectedToken]); - - const requiredCryptoAmount = useMemo(() => { - if (!formData.expectedAmount?.cryptoAmount) return 0; - - const amount = formData.expectedAmount.cryptoAmount; - // Always round UP to 7 decimal places - return Math.ceil(amount * 1e7) / 1e7; - }, [formData.expectedAmount]); - - // If backend expectedAmount isn't available, we can fall back to local - // exchange rates to estimate crypto needed: crypto = NGN amount / rate.ngn - const estimateCryptoFromRates = useCallback( - (ngnAmount: number, token: string) => { - try { - const r: any = (rates as any) || {}; - const rateFor = r[token]; - if (!rateFor || !rateFor.ngn) return 0; - const crypto = ngnAmount / parseFloat(String(rateFor.ngn)); - // Round up to 7 decimals like other logic - return Math.ceil(crypto * 1e7) / 1e7; - } catch (e) { - return 0; - } - }, - [rates] - ); - - // Find a token that has sufficient NGN-equivalent balance to cover the - // requested fiat amount. Prefers the currently selected token. - const findTokenWithSufficientBalance = useCallback( - (ngnAmount: number) => { - const rateMap: any = (rates as any) || {}; - // Build candidates from balances array - const candidates = (balances || []) - .map((b: any) => { - const token = b.chain; - const bal = parseFloat(b.balance || "0"); - const rate = rateMap[token]?.ngn ? parseFloat(rateMap[token].ngn) : 0; - return { token, bal, rate, ngnValue: bal * (rate || 0) }; - }) - .sort((a: any, b: any) => b.ngnValue - a.ngnValue); - - // Try current token first - const curr = candidates.find((c: any) => c.token === selectedToken); - if (curr && curr.ngnValue >= ngnAmount) return curr.token; - - // Otherwise pick first candidate with enough NGN value - const found = candidates.find((c: any) => c.ngnValue >= ngnAmount); - return found ? found.token : null; - }, - [balances, rates, selectedToken] - ); - - const validationError = useMemo(() => { - // Check that there's a configured merchant address for the selected token. - const merchantAddress = getToAddress(selectedToken.toLowerCase()) || ""; - - if (!currentWalletAddress) { - return "No wallet found for this currency. Add a wallet or select another currency."; - } - - if (!merchantAddress) { - // In production we require a configured merchant wallet. In development - // allow the flow to continue (auto-fallback to user's address) so - // developers can test payments locally without env vars. - if (process.env.NODE_ENV === "production") { - return `Merchant wallet for ${selectedToken.toUpperCase()} is not configured. Set NEXT_PUBLIC_${selectedToken.toUpperCase()}_WALLET or set window.__VELO_MERCHANT_WALLETS in DevTools.`; - } - // In dev, we don't block here — a dev-only fallback will be applied. - } - - if (!toAddress.trim()) { - return "Recipient address is required"; - } - - if (!formData.amount || parseFloat(formData.amount) <= 0) { - return "Amount must be greater than 0"; - } - - if (requiredCryptoAmount > currentWalletBalance) { - return "Insufficient balance"; - } - - if (type === "electricity" && config.showVerifyButton && !meterVerified) { - return "Please verify meter number first"; - } - - return null; - }, [ - currentWalletAddress, - selectedToken, - toAddress, - formData.amount, - requiredCryptoAmount, - currentWalletBalance, - type, - config.showVerifyButton, - meterVerified, - ]); - - useEffect(() => { - // Auto-fill recipient address when a token is selected or when wallet - // addresses change so the validation doesn't fail immediately on mount. - try { - const addr = getToAddress(selectedToken.toLowerCase()); - if (addr && !toAddress) { - setToAddress(addr); - } - } catch (e) { - // ignore - } - }, [selectedToken, addresses]); - - // Dev-only fallback: if there's no configured merchant address, auto-fill - // with the current user's wallet address so developers can test the flow. - useEffect(() => { - if (process.env.NODE_ENV === "production") return; - try { - const addr = getToAddress(selectedToken.toLowerCase()); - if (!addr && !toAddress && currentWalletAddress) { - setToAddress(currentWalletAddress); - setMerchantFallback(true); - } else { - setMerchantFallback(false); - } - } catch (e) { - // ignore - } - }, [selectedToken, addresses, currentWalletAddress, toAddress]); - - // When validation prevents proceeding to Confirm & Pay, surface a compact - // debug summary so developers can quickly see why without sifting through - // repeated console messages. This is intentionally a warning-level log. - useEffect(() => { - if (!validationError) return; - // Only surface developer warnings in non-production to reduce console noise - if (process.env.NODE_ENV !== "production") { - console.warn("Purchase validation blocked action:", { - validationError, - selectedToken, - toAddress, - requiredCryptoAmount, - currentWalletBalance, - currentWalletAddress, - }); - } - }, [validationError, selectedToken, toAddress, requiredCryptoAmount, currentWalletBalance, currentWalletAddress]); - - // When step 2 (payment) is entered and expectedAmount is available (or - // an NGN amount is entered), try to auto-select a token that has enough - // NGN-equivalent balance so the user doesn't hit "Insufficient balance". - useEffect(() => { - if (step !== 2) return; - const ngnAmount = parseFloat(formData.amount || "0"); - if (!ngnAmount || ngnAmount <= 0) return; - - // If backend provided expectedAmount, use that crypto amount and token - const expected = formData.expectedAmount; - if (expected && expected.cryptoAmount && expected.cryptoCurrency) { - // ensure selected token matches expected currency - const expectedToken = expected.chain || expected.cryptoCurrency?.toLowerCase(); - if (expectedToken && expectedToken !== selectedToken) { - setSelectedToken(expectedToken); - setToAddress(getToAddress(expectedToken)); - } - return; - } - - // Fallback: find a token with sufficient NGN equivalent value - const candidate = findTokenWithSufficientBalance(ngnAmount); - if (candidate && candidate !== selectedToken) { - setSelectedToken(candidate); - setToAddress(getToAddress(candidate)); - } - }, [step, formData.amount, formData.expectedAmount, findTokenWithSufficientBalance]); - - const handleSendWithPin = async (pin: string) => { - setErrorMessage(""); - - if (validationError) { - setErrorMessage(validationError); - setShowPinDialog(false); - return; - } - - setIsSending(true); - - try { - // Step 1: Send cryptocurrency transaction - let normalizedToAddress = toAddress.trim(); - - if (selectedToken === "starknet") { - try { - normalizedToAddress = normalizeStarknetAddress(toAddress, "starknet"); - } catch (error) { - throw new Error( - error instanceof Error - ? `Invalid Starknet address: ${error.message}` - : "Invalid Starknet address format" - ); - } - } - // If recipient equals the sender, handle safely depending on environment. - // - In development, simulate a transaction so developers can test the - // purchase flow without triggering backend validation (send-to-self). - // - In production, block the action to avoid accidental send-to-self. - if (normalizedToAddress === currentWalletAddress) { - if (process.env.NODE_ENV === "production") { - throw new Error("Recipient address cannot be the same as the sender."); - } - - // Dev behaviour: if we're already in a merchantFallback or the - // recipient equals the current wallet, simulate a tx so the rest of - // the purchase flow can be tested end-to-end without calling the - // backend /wallet/send with an invalid payload. - console.warn( - "Dev-mode: recipient equals sender — simulating transaction. Set a merchant wallet to test real sends." - ); - const fakeHash = `dev-tx-${Date.now()}`; - setTxHash(fakeHash); - setShowPinDialog(false); - await handleSubmitPurchase(fakeHash); - setStep(4); - return; - } - - const transactionResponse = await sendTransaction({ - chain: selectedToken, - network: currentNetwork, - toAddress: normalizedToAddress, - amount: requiredCryptoAmount.toString(), - fromAddress: currentWalletAddress, - transactionPin: pin, - }); - - setTxHash(transactionResponse.txHash); - setShowPinDialog(false); - - // Step 2: Submit purchase to backend with transaction hash - await handleSubmitPurchase(transactionResponse.txHash); - - setStep(4); - } catch (error: any) { - console.error("Transaction error:", error); - let errMsg = "Failed to send transaction. Please try again."; - - if (error.message) { - errMsg = error.message; - } else if (typeof error === "string") { - errMsg = error; - } - - setErrorMessage(errMsg); - setShowPinDialog(false); - } finally { - setIsSending(false); - } - }; - - const handleSubmitPurchase = async (transactionHash: string) => { - setLoading(true); - - try { - let response; - - if (type === "airtime") { - // Include metadata so backend has provider/expectedAmount/context - const provider = providers.find((p) => p.serviceID === formData.service_id) || null; - const metadata = { - provider, - expectedAmount: formData.expectedAmount || null, - selectedToken, - fromAddress: currentWalletAddress, - merchantAddress: getToAddress(selectedToken), - purchaseType: "AirtimePurchase", - }; - - response = await apiClient.purchaseAirtime({ - type: "airtime", - amount: parseFloat(formData.amount), - chain: selectedToken, - phoneNumber: validatePhoneNumber(formData.customer_id), - mobileNetwork: formData.service_id, - transactionHash, - metadata, - } as any); - } else if (type === "data" && formData.dataplan) { - const provider = providers.find((p) => p.serviceID === formData.service_id) || null; - const metadata = { - provider, - dataplan: formData.dataplan, - expectedAmount: formData.expectedAmount || null, - selectedToken, - fromAddress: currentWalletAddress, - merchantAddress: getToAddress(selectedToken), - purchaseType: "DataPurchase", - }; - - response = await apiClient.purchaseData({ - type: "data", - dataplanId: formData.dataplan.dataplanId, - amount: parseFloat(formData.dataplan.amount.replace(/[N₦,]/g, "")), - chain: selectedToken, - phoneNumber: validatePhoneNumber(formData.customer_id), - mobileNetwork: formData.service_id, - transactionHash, - metadata, - } as any); - } else if (type === "electricity") { - const companyInfo = electricityCompanies.find((c) => c.value === formData.service_id) || null; - const metadata = { - company: companyInfo, - expectedAmount: formData.expectedAmount || null, - selectedToken, - fromAddress: currentWalletAddress, - merchantAddress: getToAddress(selectedToken), - purchaseType: "ElectricityPurchase", - }; - - response = await apiClient.purchaseElectricity({ - type: "electricity", - amount: parseFloat(formData.amount), - chain: selectedToken, - company: formData.service_id, - meterType: formData.meterType, - meterNumber: formData.customer_id, - phoneNumber: validatePhoneNumber(formData.phoneNo), - transactionHash, - metadata, - } as any); - } else { - throw new Error("Invalid purchase type"); - } - - if (response.success) { - setSuccess(true); - - const transactionData: TransactionData = { - dateTime: new Date().toLocaleString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - hour: "numeric", - minute: "2-digit", - second: "2-digit", - }), - paymentMethod: "Cashley", - status: "Completed", - description: response.message, - transactionId: response.data.purchaseId, - providerLogo: - providers.find((p) => p.serviceID === formData.service_id)?.image || - "", - providerName: - providers.find((p) => p.serviceID === formData.service_id)?.name || - "", - planName: - response.data.planName || - formData.dataplan?.name || - `₦${formData.amount}`, - meterToken: response.data.meterToken, - }; - - setFormData((prev) => ({ ...prev, transactionData })); - } else { - setSuccess(false); - setErrorMessage(response.message || "Purchase failed"); - } - } catch (error: any) { - console.error("Purchase error:", error); - setSuccess(false); - setErrorMessage(error.message || "Failed to complete purchase"); - } finally { - setLoading(false); - } - }; - - const handleBack = () => { - if (step === 1) { - window.history.back(); - } else { - setStep((prev) => prev - 1); - setErrorMessage(""); - } - }; - - const handleNext = () => { - setErrorMessage(""); - setStep((prev) => prev + 1); - }; - - const handleConfirm = () => { - if (validationError) { - setErrorMessage(validationError); - return; - } - setShowPinDialog(true); - }; - - const data = { - serviceId: formData.service_id, - customerId: formData.customer_id, - }; - - // console.log("data", data); - - const isStep1Valid = () => { - if (!formData.service_id) return false; - if (!formData.customer_id) return false; - - if (type === "data" && !formData.dataplan) return false; - if ((type === "airtime" || type === "electricity") && !formData.amount) - return false; - if ( - type === "electricity" && - config.showVerifyButton && - !meterVerified && - !formData.phoneNo - ) - return false; - - return true; - }; - - const renderStep = () => { - switch (step) { - case 1: - return ( - - {/* Provider Selection */} -
- -
- {providers.map((provider) => ( - - setFormData((prev) => ({ ...prev, service_id: provider.serviceID })) - } - className={`flex-shrink-0 p-4 rounded-2xl transition-all ${ - formData.service_id === provider.serviceID - ? "bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-lg ring-2 ring-white/50" - : "bg-white/5 text-gray-300 hover:bg-white/10 border border-white/10" - }`} - > -
-
{provider.name[0]}
-
{provider.name}
-
-
- ))} -
-
- - {/* Amount Grid */} - {config.showAmountGrid && formData.service_id && ( -
- -
- {presetAmounts.map((amount) => ( - setFormData((prev) => ({ ...prev, amount: amount.toString() }))} - className={`p-3 rounded-xl transition-all ${ - formData.amount === amount.toString() - ? "bg-gradient-to-r from-cyan-500 to-blue-500 text-white shadow-lg" - : "bg-white/5 text-gray-300 hover:bg-white/10 border border-white/10" - }`} - > - ₦{amount.toLocaleString()} - - ))} -
-
- )} - - {/* Phone/Meter Number Input */} - {formData.service_id && (config.showAmountGrid || type === "data") && ( -
- -
- {type !== "electricity" && ( -
234
- )} - - setFormData((prev) => ({ ...prev, customer_id: e.target.value })) - } - placeholder={config.placeholder} - type="tel" - className="flex-1 p-4 rounded-xl bg-white/10 backdrop-blur-sm border border-white/20 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500" - /> -
-
- )} - - {/* Phone Number for Electricity */} - {type === "electricity" && formData.customer_id && ( -
- - - setFormData((prev) => ({ ...prev, phoneNo: e.target.value })) - } - placeholder="08123456789" - type="tel" - className="w-full p-4 rounded-xl bg-white/10 backdrop-blur-sm border border-white/20 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500" - /> -
- )} - - {errorMessage && ( -
- - {errorMessage} -
- )} - - - Continue - - -
- ); - - case 2: - return ( - -
- - - -

Select Payment Method

-
- -
-
-
-

Amount

-

₦{parseInt(formData.amount || "0").toLocaleString()}

-
-
-

Provider

-

{providers.find(p => p.serviceID === formData.service_id)?.name}

-
-
-
- -
- {["Ethereum", "Bitcoin", "USDT", "Solana"].map((crypto) => ( - setSelectedToken(crypto.toLowerCase())} - className={`p-6 rounded-2xl transition-all ${ - selectedToken === crypto.toLowerCase() - ? "bg-gradient-to-br from-cyan-500 to-blue-500 text-white shadow-lg" - : "bg-white/5 text-gray-300 hover:bg-white/10 border border-white/10" - }`} - > -
-
{crypto[0]}
-
{crypto}
-
-
- ))} -
- -
- - Back - - - Continue - - -
-
- ); - - case 3: - return ( - -
- - - -

Transaction Summary

-
- -
-
- Product - {type} -
-
- Provider - {providers.find(p => p.serviceID === formData.service_id)?.name} -
-
- {config.customerLabel} - {type !== "electricity" ? `234${formData.customer_id}` : formData.customer_id} -
-
- Payment Method - {selectedToken} -
-
- Total Amount - ₦{parseInt(formData.amount || "0").toLocaleString()} -
-
- -
- - Back - - - Confirm & Pay - - -
-
- ); - - case 4: - return ( - - - {success ? ( -
- -
- ) : ( -
- -
- )} -
- -
-

- {success ? "Transaction Successful!" : "Transaction Failed"} -

-

- {success - ? "Your transaction has been processed successfully" - : "Something went wrong. Please try again."} -

-
- - {success && ( -
-
- Amount - ₦{parseInt(formData.amount || "0").toLocaleString()} -
-
- Provider - {providers.find(p => p.serviceID === formData.service_id)?.name} -
-
- Status - Completed -
-
- )} - - - - New Purchase - - - {/* Confetti effect */} - {success && ( -
- {[...Array(20)].map((_, i) => ( - - ))} -
- )} -
- ); - - default: - return null; - } - }; - - return ( -
- {/* PIN Dialog */} - {showPinDialog && ( - setShowPinDialog(false)} - onSubmit={handleSendWithPin} - isLoading={isSending} - /> - )} - - {renderStep()} -
- ); -} - -// PIN Dialog Component -function PinDialog({ - onClose, - onSubmit, - isLoading, -}: { - onClose: () => void; - onSubmit: (pin: string) => void; - isLoading: boolean; -}) { - const [pin, setPin] = useState(["", "", "", ""]); - const inputRefs = React.useRef<(HTMLInputElement | null)[]>([]); - - const handleChange = (index: number, value: string) => { - if (value.length > 1) return; - if (value && !/^\d$/.test(value)) return; - - const newPin = [...pin]; - newPin[index] = value; - setPin(newPin); - - if (value && index < 3) { - inputRefs.current[index + 1]?.focus(); - } - - if (newPin.every((digit) => digit !== "") && index === 3) { - onSubmit(newPin.join("")); - } - }; - - return ( - - e.stopPropagation()} - className="bg-gradient-to-br from-gray-900 to-gray-800 rounded-3xl p-8 max-w-md w-full border border-white/20" - > -

Enter PIN

- -
- {pin.map((digit, index) => ( - (inputRefs.current[index] = el)} - type="password" - inputMode="numeric" - maxLength={1} - value={digit} - onChange={(e) => handleChange(index, e.target.value)} - disabled={isLoading} - className="w-16 h-20 text-center bg-white/10 backdrop-blur-sm border-2 border-white/20 rounded-2xl text-white text-2xl focus:outline-none focus:border-purple-500 focus:ring-4 focus:ring-purple-500/30 transition-all" - /> - ))} -
- - {isLoading && ( -
- - Processing... -
- )} -
-
- ); -} - - -// === MISSING SUB-COMPONENTS (Added) === - -function DataPlanSelect({ - dataPlans, - value, - onSelect, - loading, -}: { - dataPlans: DataPlan[]; - value: DataPlan | null; - onSelect: (plan: DataPlan) => void; - loading: boolean; -}) { - if (loading) { - return ( -
- -
- {[...Array(4)].map((_, i) => ( -
-
-
- ))} -
-
- ); - } - - return ( -
- -
- {dataPlans.map((plan) => ( -
- onSelect(plan)} - className="w-full p-4 rounded-2xl text-left transition-all " - > -
-
-

{plan.name}

-

{plan.validity}

-
- - ₦ - {parseInt(plan.amount.replace(/[N₦,]/g, "")).toLocaleString()} - -
-
-
- ))} -
-
- ); -} - -function MeterTypeSelect({ - meterTypes, - value, - onChange, -}: { - meterTypes: MeterType[]; - value: "prepaid" | "postpaid"; - onChange: (type: "prepaid" | "postpaid") => void; -}) { - return ( -
- -
- {meterTypes.map((type) => ( - onChange(type.value as "prepaid" | "postpaid")} - className={`p-4 rounded-2xl font-bold transition-all ${ - value === type.value - ? "ring-2 ring-blue-500 text-blue-700" - : " text-gray-700" - }`} - > - {type.label} - - ))} -
-
- ); -} - -// === REUSABLE TRANSACTION DETAIL COMPONENT === -function TransactionDetail({ - label, - value, - monospace = false, - link, - className = "", -}: { - label: string; - value: string; - monospace?: boolean; - link?: string; - className?: string; -}) { - return ( -
- {label} - {link ? ( - - {value.slice(0, 10)}...{value.slice(-8)} - - ) : ( - - {value} - - )} -
- ); -} - -// Sub-components -function ProviderSelect({ - providers, - value, - onChange, - loading, -}: { - providers: Provider[]; - value: string; - onChange: (service_id: string) => void; - loading: boolean; -}) { - if (loading) { - return ( -
-
- {[...Array(4)].map((_, i) => ( -
-
-
- ))} -
-
- ); - } - return ( -
-
- {providers.map((provider) => ( -
- onChange(provider.serviceID)} - className="flex flex-col items-center w-full rounded-2xl overflow-hidden transition-all " - > -
- {provider.name} { - // Fallback to a default image or show provider name - e.currentTarget.style.display = "none"; - e.currentTarget.parentElement!.innerHTML = `
${provider.name.substring( - 0, - 3 - )}
`; - }} - /> -
- - {provider.name} - -
-
- ))} -
-
- ); -} - -function AmountGrid({ - value, - onChange, - presetAmounts, - minAmount, - maxAmount, -}: { - value: string; - onChange: (amount: string) => void; - presetAmounts: number[]; - minAmount?: number; - maxAmount?: number; -}) { - const numericValue = parseFloat(value) || 0; - - const isBelowMin = - minAmount !== undefined && numericValue > 0 && numericValue < minAmount; - const isAboveMax = maxAmount !== undefined && numericValue > maxAmount; - - const filteredPresets = presetAmounts.filter((amount) => { - if (minAmount !== undefined && amount < minAmount) return false; - if (maxAmount !== undefined && amount > maxAmount) return false; - return true; - }); - - return ( -
-
- - {(minAmount || maxAmount) && ( - - {minAmount && `Min: ₦${minAmount.toLocaleString()}`} - {minAmount && maxAmount && " • "} - {maxAmount && `Max: ₦${maxAmount.toLocaleString()}`} - - )} -
- -
- {filteredPresets.length > 0 ? ( - filteredPresets.map((amount) => ( - onChange(amount.toString())} - className={`p-4 rounded-2xl transition-all font-medium ${ - value === amount.toString() - ? "font-black ring-2 ring-blue-500" - : "" - }`} - > - ₦{amount.toLocaleString()} - - )) - ) : ( -

- No preset amounts available -

- )} -
- -
- { - const val = e.target.value; - if (val === "" || /^\d*$/.test(val)) { - onChange(val); - } - }} - placeholder="Enter custom amount" - type="text" - inputMode="numeric" - className={`w-full p-4 rounded-2xl border-2 pr-10 transition-all ${ - isBelowMin || isAboveMax ? "border-red-500 " : "border-transparent " - } outline-none`} - /> - {(isBelowMin || isAboveMax) && ( -
- -
- )} -
- - {(isBelowMin || isAboveMax) && ( -

- {isBelowMin - ? `Amount must be at least ₦${minAmount?.toLocaleString()}` - : `Amount cannot exceed ₦${maxAmount?.toLocaleString()}`} -

- )} -
- ); -} - -function ReviewItem({ label, value }: { label: string; value: string }) { - return ( -
- {label} - {value} -
- ); -} - -function Button({ - onclick, - text, - disabled, - type, -}: { - onclick: () => void; - text: string; - disabled?: boolean; - type?: "secondary"; -}) { - const baseClasses = - "w-full py-4 rounded-full font-bold text-lg relative transition-all"; - const typeClasses = - type === "secondary" - ? "bg-gray-200 text-black hover:bg-gray-300" - : "0 text-white hover:bg-blue-600"; - const disabledClasses = disabled - ? "opacity-50 cursor-not-allowed" - : "cursor-pointer"; - return ( - - ); -} diff --git a/service/blockchain-manager.ts b/service/blockchain-manager.ts deleted file mode 100644 index e483f50..0000000 --- a/service/blockchain-manager.ts +++ /dev/null @@ -1,92 +0,0 @@ -// services/blockchain-manager.ts -import { BlockchainProvider, WalletMonitorResult } from "@/types/multi-chain"; -import { StarknetProvider } from "@/components/providers/starknet-provider"; -import { EthereumProvider } from "@/components/providers/ethereum-provider"; -import { SolanaProvider } from "@/components/providers/solana-provider"; - -export class BlockchainManager { - private providers: Map = new Map(); - - constructor() { - this.initializeProviders(); - } - - private initializeProviders() { - // Starknet - - - // Ethereum - if (process.env.ETHEREUM_NODE_URL) { - const ethereumProvider = new EthereumProvider( - process.env.ETHEREUM_NODE_URL, - process.env.ETHEREUM_CHAIN_ID || "sepolia" - ); - this.providers.set("ethereum", ethereumProvider); - } - - // Polygon (EVM-compatible) - if (process.env.POLYGON_NODE_URL) { - const polygonProvider = new EthereumProvider( - process.env.POLYGON_NODE_URL, - process.env.POLYGON_CHAIN_ID || "polygon-mumbai" - ); - this.providers.set("polygon", polygonProvider); - } - - // Solana - if (process.env.SOLANA_NODE_URL) { - const solanaProvider = new SolanaProvider(process.env.SOLANA_NODE_URL); - this.providers.set("solana", solanaProvider); -} - } - - async monitorWallet( - chain: string, - walletAddress: string, - fromBlock?: number - ): Promise { - const provider = this.providers.get(chain); - - if (!provider) { - return { - status: "error", - walletAddress, - transactions: [], - totalTransactions: 0, - scannedBlocks: { from: 0, to: 0 }, - error: `Unsupported blockchain: ${chain}. Supported: ${Array.from(this.providers.keys()).join(", ")}` - }; - } - - if (!provider.isValidAddress(walletAddress)) { - return { - status: "error", - walletAddress, - transactions: [], - totalTransactions: 0, - scannedBlocks: { from: 0, to: 0 }, - error: `Invalid address format for ${chain}` - }; - } - - return await provider.monitorWalletTransactions(walletAddress, fromBlock); - } - - getSupportedChains(): string[] { - return Array.from(this.providers.keys()); - } - - async testAllConnections(): Promise<{ [chain: string]: boolean }> { - const results: { [chain: string]: boolean } = {}; - - for (const [chain, provider] of this.providers) { - try { - results[chain] = await provider.testConnection(); - } catch { - results[chain] = false; - } - } - - return results; - } -} \ No newline at end of file