diff --git a/.cursorrules b/.cursorrules index 6ecc152d0..721547acd 100644 --- a/.cursorrules +++ b/.cursorrules @@ -21,6 +21,13 @@ - **Performance is important** - cache where possible, make sure to not make unnecessary re-renders or data fetching. - **Flag breaking changes** - always flag if changes done in Frontend are breaking and require action on Backend (or viceversa) +## ๐Ÿšซ Import Rules (critical for build performance) + +- **No barrel imports** - never use `import * as X from '@/constants'` or create index.ts barrel files. always import from specific files (e.g. `import { PEANUT_API_URL } from '@/constants/general.consts'`). barrel imports slow down builds and cause bundling issues. +- **No circular dependencies** - before adding imports, check if the target file imports from the current file. circular deps cause `Cannot access X before initialization` errors. move shared types to `interfaces.ts` if needed. +- **No node.js packages in client components** - packages like `web-push`, `fs`, `crypto` (node) can't be used in `'use client'` files. use server actions or api routes instead. +- **Check for legacy code** - before importing from a file, check if it has TODO comments marking it as legacy/deprecated. prefer newer implementations. + ## ๐Ÿงช Testing - **Test new code** - where tests make sense, test new code. Especially with fast unit tests. @@ -43,6 +50,7 @@ - **Cache where possible** - avoid unnecessary re-renders and data fetching - **Fire simultaneous requests** - if you're doing multiple sequential awaits and they're not interdependent, fire them simultaneously - **Service Worker cache version** - only bump `NEXT_PUBLIC_API_VERSION` for breaking API changes (see JSDoc in `src/app/sw.ts`). Users auto-migrate. +- **Gate heavy features in dev** - prefetching, precompiling, or eager loading of routes can add 5-10s to dev cold starts. wrap with `process.env.NODE_ENV !== 'development'` (e.g. `` in layout.tsx). ## ๐Ÿ“ Commits diff --git a/.gitignore b/.gitignore index 1bb7d213f..811e26f55 100644 --- a/.gitignore +++ b/.gitignore @@ -69,4 +69,4 @@ certificates public/sw* public/swe-worker* -.idea \ No newline at end of file +.idea diff --git a/knip.json b/knip.json index 82c3a2d67..6ef028c5e 100644 --- a/knip.json +++ b/knip.json @@ -1,6 +1,6 @@ { "$schema": "https://unpkg.com/knip@5.37.1/schema.json", "ignore": ["src/assets/**"], - "entry": ["src/**/*.{js,jsx,ts,tsx}", "postcss.config.js", "tailwind.config.js"], + "entry": ["src/**/*.{js,jsx,ts,tsx}"], "project": ["src/**/*.{js,jsx,ts,tsx}", "*.config.{js,ts}"] } diff --git a/next.config.js b/next.config.js index b3e0d3715..67f71efdc 100644 --- a/next.config.js +++ b/next.config.js @@ -58,9 +58,6 @@ let nextConfig = { }, }, - // External packages that shouldn't be bundled (server-side only) - serverExternalPackages: [], - // Disable source maps in production (already handled by Sentry) productionBrowserSourceMaps: false, @@ -69,9 +66,24 @@ let nextConfig = { // Experimental features for optimization experimental: { - // Optimize package imports for tree-shaking - optimizePackageImports: ['@chakra-ui/react', 'framer-motion', '@headlessui/react'], - // Speed up webpack builds (fallback mode when not using --turbo) + // Note: turbopackFileSystemCacheForDev is enabled by default in Next.js 16+ + // optimize package imports for tree-shaking (barrel file optimization) + // lodash and date-fns are used by transitive dependencies (e.g. chakra, framer-motion) + optimizePackageImports: [ + '@chakra-ui/react', + 'framer-motion', + '@headlessui/react', + '@radix-ui/react-accordion', + '@radix-ui/react-select', + '@radix-ui/react-slider', + '@reduxjs/toolkit', + 'react-redux', + 'lodash', + 'date-fns', + 'react-hook-form', + '@mui/icons-material', + ], + // Speed up webpack builds (used for production builds with --webpack flag) webpackBuildWorker: true, }, diff --git a/package.json b/package.json index 50ccb2aab..c5f91a71b 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ }, "scripts": { "dev": "NODE_OPTIONS=\"--max-old-space-size=8192\" next dev --turbo", + "dev:clean": "rm -rf .next && NODE_OPTIONS=\"--max-old-space-size=8192\" next dev --turbo", "dev:fallback": "next dev", "build": "next build --webpack", "start": "next start", @@ -20,23 +21,20 @@ "script": "NODE_OPTIONS=\"--experimental-json-modules\" tsx" }, "dependencies": { - "@calcom/embed-react": "^1.5.1", - "@chakra-ui/color-mode": "^2.2.0", - "@chakra-ui/icon": "^3.2.0", - "@chakra-ui/react": "^2.10.4", - "@chakra-ui/react-context": "^2.1.0", - "@chakra-ui/shared-utils": "^2.0.4", "@daimo/pay": "^1.16.5", "@dicebear/collection": "^9.2.2", "@dicebear/core": "^9.2.2", + "@emotion/react": "^11.14.0", "@headlessui/react": "^2.2.9", "@headlessui/tailwindcss": "^0.2.1", - "@hookform/resolvers": "3.9.1", "@justaname.id/react": "0.3.180", "@justaname.id/sdk": "0.2.177", + "@mui/icons-material": "^7.3.6", + "@mui/material": "^7.3.6", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-tabs": "^1.1.13", "@reduxjs/toolkit": "^2.5.0", "@reown/appkit": "1.6.9", "@reown/appkit-adapter-wagmi": "1.6.9", @@ -51,10 +49,8 @@ "@wagmi/core": "2.19.0", "@zerodev/passkey-validator": "^5.6.0", "@zerodev/sdk": "5.5.0", - "auto-text-size": "^0.2.3", "autoprefixer": "^10.4.20", "canvas-confetti": "^1.9.3", - "chakra-ui-steps": "^2.1.0", "classnames": "^2.5.1", "d3-force": "^3.0.0", "embla-carousel-react": "^8.6.0", @@ -64,16 +60,12 @@ "iban-to-bic": "^1.4.0", "js-cookie": "^3.0.5", "jsqr": "^1.4.0", - "lottie-react": "^2.4.0", - "multicoin-address-validator": "^0.5.22", "next": "16.0.10", "pulltorefreshjs": "^0.1.22", "react": "^19.2.1", - "react-csv": "^2.2.2", "react-dom": "^19.2.1", "react-fast-marquee": "^1.6.5", "react-force-graph-2d": "^1.25.10", - "react-ga4": "^2.1.0", "react-hook-form": "^7.53.2", "react-onesignal": "^3.2.3", "react-qr-code": "^2.0.15", @@ -84,39 +76,29 @@ "tailwind-merge": "^1.14.0", "tailwind-scrollbar": "^3.1.0", "use-haptic": "^1.1.11", - "uuid": "^10.0.0", "validator": "^13.12.0", "vaul": "^1.1.2", "viem": "^2.22.0", - "wagmi": "2.16.3", - "web-push": "^3.6.7", - "yup": "^1.4.0" + "wagmi": "2.16.3" }, "devDependencies": { - "@next/bundle-analyzer": "^16.0.0", + "@next/bundle-analyzer": "^16.1.1", "@serwist/build": "^9.0.10", "@size-limit/preset-app": "^11.2.0", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^16.1.0", - "@testing-library/user-event": "^14.5.2", "@types/canvas-confetti": "^1.9.0", - "@types/chroma-js": "^2.4.4", "@types/jest": "^29.5.12", "@types/js-cookie": "^3.0.6", - "@types/multicoin-address-validator": "^0.5.3", "@types/node": "20.4.2", "@types/pulltorefreshjs": "^0.1.7", "@types/react": "^18.3.12", - "@types/react-csv": "^1.1.10", "@types/react-dom": "^18.3.1", - "@types/uuid": "^9.0.8", "@types/validator": "^13.12.2", - "@types/web-push": "^3.6.4", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-transform-stub": "^2.0.0", "knip": "^5.37.1", - "ngrok": "^4.0.0", "postcss": "^8.4.49", "postcss-import": "^16.1.0", "prettier": "^3.3.3", @@ -125,7 +107,6 @@ "size-limit": "^11.2.0", "tailwindcss": "^3.4.15", "ts-jest": "^29.1.2", - "tslib": "^2.7.0", "tsx": "^4.19.3", "typescript": "^5.6.3" }, @@ -151,7 +132,6 @@ "^@squirrel-labs/peanut-sdk$": "/src/utils/__mocks__/peanut-sdk.ts", "^@reown/appkit/react$": "/src/utils/__mocks__/reown-appkit.ts", "^@justaname\\.id/react$": "/src/utils/__mocks__/justaname.ts", - "^web-push$": "/src/utils/__mocks__/web-push.ts", "^next/cache$": "/src/utils/__mocks__/next-cache.ts", "^@zerodev/sdk(.*)$": "/src/utils/__mocks__/zerodev-sdk.ts", "^@simplewebauthn/browser$": "/src/utils/__mocks__/simplewebauthn-browser.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28debb68e..70dc59441 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,48 +14,36 @@ importers: .: dependencies: - '@calcom/embed-react': - specifier: ^1.5.1 - version: 1.5.3(react-dom@19.2.1(react@19.2.1))(react@19.2.1) - '@chakra-ui/color-mode': - specifier: ^2.2.0 - version: 2.2.0(react@19.2.1) - '@chakra-ui/icon': - specifier: ^3.2.0 - version: 3.2.0(@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(react@19.2.1))(react@19.2.1) - '@chakra-ui/react': - specifier: ^2.10.4 - version: 2.10.9(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) - '@chakra-ui/react-context': - specifier: ^2.1.0 - version: 2.1.0(react@19.2.1) - '@chakra-ui/shared-utils': - specifier: ^2.0.4 - version: 2.0.4 '@daimo/pay': specifier: ^1.16.5 - version: 1.16.5(05566c87f67e0ead90f6e2b4ff43e130) + version: 1.16.5(e6cc8f89f2a748207ee80ab4acbea829) '@dicebear/collection': specifier: ^9.2.2 version: 9.2.4(@dicebear/core@9.2.4) '@dicebear/core': specifier: ^9.2.2 version: 9.2.4 + '@emotion/react': + specifier: ^11.14.0 + version: 11.14.0(@types/react@18.3.23)(react@19.2.1) '@headlessui/react': specifier: ^2.2.9 version: 2.2.9(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@headlessui/tailwindcss': specifier: ^0.2.1 version: 0.2.2(tailwindcss@3.4.17) - '@hookform/resolvers': - specifier: 3.9.1 - version: 3.9.1(react-hook-form@7.62.0(react@19.2.1)) '@justaname.id/react': specifier: 0.3.180 - version: 0.3.180(3309f1e2f8bb0def69834732f15d7315) + version: 0.3.180(f21193980fd875d23a1e8d26c751ddf1) '@justaname.id/sdk': specifier: 0.2.177 version: 0.2.177(ethers@5.7.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(siwe@2.3.2(ethers@5.7.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@mui/icons-material': + specifier: ^7.3.6 + version: 7.3.6(@mui/material@7.3.6(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(@types/react@18.3.23)(react@19.2.1) + '@mui/material': + specifier: ^7.3.6 + version: 7.3.6(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-accordion': specifier: ^1.2.12 version: 1.2.12(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -65,15 +53,18 @@ importers: '@radix-ui/react-slider': specifier: ^1.3.5 version: 1.3.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-tabs': + specifier: ^1.1.13 + version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@reduxjs/toolkit': specifier: ^2.5.0 version: 2.8.2(react-redux@9.2.0(@types/react@18.3.23)(react@19.2.1)(redux@5.0.1))(react@19.2.1) '@reown/appkit': specifier: 1.6.9 - version: 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-adapter-wagmi': specifier: 1.6.9 - version: 1.6.9(1120c5d6420457d1541c4aa99b99d00c) + version: 1.6.9(3a722632bd268f29b4a2886282b3288f) '@safe-global/safe-apps-sdk': specifier: ^9.1.0 version: 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) @@ -91,7 +82,7 @@ importers: version: 0.6.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@tanstack/react-query': specifier: 5.8.4 - version: 5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) + version: 5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) '@typeform/embed-react': specifier: ^3.20.0 version: 3.20.0(react@19.2.1) @@ -107,18 +98,12 @@ importers: '@zerodev/sdk': specifier: 5.5.0 version: 5.5.0(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) - auto-text-size: - specifier: ^0.2.3 - version: 0.2.3(react@19.2.1) autoprefixer: specifier: ^10.4.20 version: 10.4.21(postcss@8.5.6) canvas-confetti: specifier: ^1.9.3 version: 1.9.3 - chakra-ui-steps: - specifier: ^2.1.0 - version: 2.2.0(@chakra-ui/react@2.10.9(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) classnames: specifier: ^2.5.1 version: 2.5.1 @@ -146,12 +131,6 @@ importers: jsqr: specifier: ^1.4.0 version: 1.4.0 - lottie-react: - specifier: ^2.4.0 - version: 2.4.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1) - multicoin-address-validator: - specifier: ^0.5.22 - version: 0.5.26 next: specifier: 16.0.10 version: 16.0.10(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -161,9 +140,6 @@ importers: react: specifier: ^19.2.1 version: 19.2.1 - react-csv: - specifier: ^2.2.2 - version: 2.2.2 react-dom: specifier: ^19.2.1 version: 19.2.1(react@19.2.1) @@ -173,9 +149,6 @@ importers: react-force-graph-2d: specifier: ^1.25.10 version: 1.29.0(react@19.2.1) - react-ga4: - specifier: ^2.1.0 - version: 2.1.0 react-hook-form: specifier: ^7.53.2 version: 7.62.0(react@19.2.1) @@ -206,9 +179,6 @@ importers: use-haptic: specifier: ^1.1.11 version: 1.1.11 - uuid: - specifier: ^10.0.0 - version: 10.0.0 validator: specifier: ^13.12.0 version: 13.15.15 @@ -220,17 +190,11 @@ importers: version: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: specifier: 2.16.3 - version: 2.16.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.8.3)(@tanstack/react-query@5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) - web-push: - specifier: ^3.6.7 - version: 3.6.7 - yup: - specifier: ^1.4.0 - version: 1.7.0 + version: 2.16.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.8.3)(@tanstack/react-query@5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) devDependencies: '@next/bundle-analyzer': - specifier: ^16.0.0 - version: 16.0.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + specifier: ^16.1.1 + version: 16.1.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@serwist/build': specifier: ^9.0.10 version: 9.1.1(typescript@5.9.2) @@ -243,24 +207,15 @@ importers: '@testing-library/react': specifier: ^16.1.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) - '@testing-library/user-event': - specifier: ^14.5.2 - version: 14.6.1(@testing-library/dom@10.4.1) '@types/canvas-confetti': specifier: ^1.9.0 version: 1.9.0 - '@types/chroma-js': - specifier: ^2.4.4 - version: 2.4.5 '@types/jest': specifier: ^29.5.12 version: 29.5.14 '@types/js-cookie': specifier: ^3.0.6 version: 3.0.6 - '@types/multicoin-address-validator': - specifier: ^0.5.3 - version: 0.5.3 '@types/node': specifier: 20.4.2 version: 20.4.2 @@ -270,21 +225,12 @@ importers: '@types/react': specifier: ^18.3.12 version: 18.3.23 - '@types/react-csv': - specifier: ^1.1.10 - version: 1.1.10 '@types/react-dom': specifier: ^18.3.1 version: 18.3.7(@types/react@18.3.23) - '@types/uuid': - specifier: ^9.0.8 - version: 9.0.8 '@types/validator': specifier: ^13.12.2 version: 13.15.2 - '@types/web-push': - specifier: ^3.6.4 - version: 3.6.4 jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.4.2)(babel-plugin-macros@3.1.0) @@ -297,9 +243,6 @@ importers: knip: specifier: ^5.37.1 version: 5.62.0(@types/node@20.4.2)(typescript@5.9.2) - ngrok: - specifier: ^4.0.0 - version: 4.3.3 postcss: specifier: ^8.4.49 version: 8.5.6 @@ -324,9 +267,6 @@ importers: ts-jest: specifier: ^29.1.2 version: 29.4.1(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.3))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.4.2)(babel-plugin-macros@3.1.0))(typescript@5.9.2) - tslib: - specifier: ^2.7.0 - version: 2.8.1 tsx: specifier: ^4.19.3 version: 4.20.4 @@ -822,117 +762,6 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@calcom/embed-core@1.5.3': - resolution: {integrity: sha512-GeId9gaByJ5EWiPmuvelZOvFWPOTWkcWZr5vGTCbIUTX125oE5yn0n8lDF1MJk5Xj1WO+/dk9jKIE08Ad9ytiQ==} - - '@calcom/embed-react@1.5.3': - resolution: {integrity: sha512-JCgge04pc8fhdvUmPNVLhW8/lCWK+AAziKecKWWPfv1nn2s+qKP2BwsEAnxhxK9yPOBgE1EIEgmYkrrNB1iajA==} - peerDependencies: - react: ^18.2.0 || ^19.0.0 - react-dom: ^18.2.0 || ^19.0.0 - - '@calcom/embed-snippet@1.3.3': - resolution: {integrity: sha512-pqqKaeLB8R6BvyegcpI9gAyY6Xyx1bKYfWvIGOvIbTpguWyM1BBBVcT9DCeGe8Zw7Ujp5K56ci7isRUrT2Uadg==} - - '@chakra-ui/anatomy@2.2.2': - resolution: {integrity: sha512-MV6D4VLRIHr4PkW4zMyqfrNS1mPlCTiCXwvYGtDFQYr+xHFfonhAuf9WjsSc0nyp2m0OdkSLnzmVKkZFLo25Tg==} - - '@chakra-ui/anatomy@2.3.6': - resolution: {integrity: sha512-TjmjyQouIZzha/l8JxdBZN1pKZTj7sLpJ0YkFnQFyqHcbfWggW9jKWzY1E0VBnhtFz/xF3KC6UAVuZVSJx+y0g==} - - '@chakra-ui/color-mode@2.2.0': - resolution: {integrity: sha512-niTEA8PALtMWRI9wJ4LL0CSBDo8NBfLNp4GD6/0hstcm3IlbBHTVKxN6HwSaoNYfphDQLxCjT4yG+0BJA5tFpg==} - peerDependencies: - react: '>=18' - - '@chakra-ui/hooks@2.4.5': - resolution: {integrity: sha512-601fWfHE2i7UjaxK/9lDLlOni6vk/I+04YDbM0BrelJy+eqxdlOmoN8Z6MZ3PzFh7ofERUASor+vL+/HaCaZ7w==} - peerDependencies: - react: '>=18' - - '@chakra-ui/icon@3.2.0': - resolution: {integrity: sha512-xxjGLvlX2Ys4H0iHrI16t74rG9EBcpFvJ3Y3B7KMQTrnW34Kf7Da/UC8J67Gtx85mTHW020ml85SVPKORWNNKQ==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - - '@chakra-ui/object-utils@2.1.0': - resolution: {integrity: sha512-tgIZOgLHaoti5PYGPTwK3t/cqtcycW0owaiOXoZOcpwwX/vlVb+H1jFsQyWiiwQVPt9RkoSLtxzXamx+aHH+bQ==} - - '@chakra-ui/react-context@2.1.0': - resolution: {integrity: sha512-iahyStvzQ4AOwKwdPReLGfDesGG+vWJfEsn0X/NoGph/SkN+HXtv2sCfYFFR9k7bb+Kvc6YfpLlSuLvKMHi2+w==} - peerDependencies: - react: '>=18' - - '@chakra-ui/react-use-safe-layout-effect@2.1.0': - resolution: {integrity: sha512-Knbrrx/bcPwVS1TorFdzrK/zWA8yuU/eaXDkNj24IrKoRlQrSBFarcgAEzlCHtzuhufP3OULPkELTzz91b0tCw==} - peerDependencies: - react: '>=18' - - '@chakra-ui/react-utils@2.0.12': - resolution: {integrity: sha512-GbSfVb283+YA3kA8w8xWmzbjNWk14uhNpntnipHCftBibl0lxtQ9YqMFQLwuFOO0U2gYVocszqqDWX+XNKq9hw==} - peerDependencies: - react: '>=18' - - '@chakra-ui/react@2.10.9': - resolution: {integrity: sha512-lhdcgoocOiURwBNR3L8OioCNIaGCZqRfuKioLyaQLjOanl4jr0PQclsGb+w0cmito252vEWpsz2xRqF7y+Flrw==} - peerDependencies: - '@emotion/react': '>=11' - '@emotion/styled': '>=11' - framer-motion: '>=4.0.0' - react: '>=18' - react-dom: '>=18' - - '@chakra-ui/shared-utils@2.0.4': - resolution: {integrity: sha512-JGWr+BBj3PXGZQ2gxbKSD1wYjESbYsZjkCeE2nevyVk4rN3amV1wQzCnBAhsuJktMaZD6KC/lteo9ou9QUDzpA==} - - '@chakra-ui/shared-utils@2.0.5': - resolution: {integrity: sha512-4/Wur0FqDov7Y0nCXl7HbHzCg4aq86h+SXdoUeuCMD3dSj7dpsVnStLYhng1vxvlbUnLpdF4oz5Myt3i/a7N3Q==} - - '@chakra-ui/styled-system@2.12.4': - resolution: {integrity: sha512-oa07UG7Lic5hHSQtGRiMEnYjuhIa8lszyuVhZjZqR2Ap3VMF688y1MVPJ1pK+8OwY5uhXBgVd5c0+rI8aBZlwg==} - - '@chakra-ui/styled-system@2.9.2': - resolution: {integrity: sha512-To/Z92oHpIE+4nk11uVMWqo2GGRS86coeMmjxtpnErmWRdLcp1WVCVRAvn+ZwpLiNR+reWFr2FFqJRsREuZdAg==} - - '@chakra-ui/system@2.6.2': - resolution: {integrity: sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==} - peerDependencies: - '@emotion/react': ^11.0.0 - '@emotion/styled': ^11.0.0 - react: '>=18' - - '@chakra-ui/theme-tools@2.1.2': - resolution: {integrity: sha512-Qdj8ajF9kxY4gLrq7gA+Azp8CtFHGO9tWMN2wfF9aQNgG9AuMhPrUzMq9AMQ0MXiYcgNq/FD3eegB43nHVmXVA==} - peerDependencies: - '@chakra-ui/styled-system': '>=2.0.0' - - '@chakra-ui/theme-tools@2.2.9': - resolution: {integrity: sha512-PcbYL19lrVvEc7Oydy//jsy/MO/rZz1DvLyO6AoI+bI/+Kwz9WfOKsspbulEhRg5COayE0R/IZPsskXZ7Mp4bA==} - peerDependencies: - '@chakra-ui/styled-system': '>=2.0.0' - - '@chakra-ui/theme-utils@2.0.21': - resolution: {integrity: sha512-FjH5LJbT794r0+VSCXB3lT4aubI24bLLRWB+CuRKHijRvsOg717bRdUN/N1fEmEpFnRVrbewttWh/OQs0EWpWw==} - - '@chakra-ui/theme@3.3.1': - resolution: {integrity: sha512-Hft/VaT8GYnItGCBbgWd75ICrIrIFrR7lVOhV/dQnqtfGqsVDlrztbSErvMkoPKt0UgAkd9/o44jmZ6X4U2nZQ==} - peerDependencies: - '@chakra-ui/styled-system': '>=2.8.0' - - '@chakra-ui/theme@3.4.9': - resolution: {integrity: sha512-GAom2SjSdRWTcX76/2yJOFJsOWHQeBgaynCUNBsHq62OafzvELrsSHDUw0bBqBb1c2ww0CclIvGilPup8kXBFA==} - peerDependencies: - '@chakra-ui/styled-system': '>=2.8.0' - - '@chakra-ui/utils@2.0.15': - resolution: {integrity: sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA==} - - '@chakra-ui/utils@2.2.5': - resolution: {integrity: sha512-KTBCK+M5KtXH6p54XS39ImQUMVtAx65BoZDoEms3LuObyTo1+civ1sMm4h3nRT320U6H5H7D35WnABVQjqU/4g==} - peerDependencies: - react: '>=16.8.0' - '@coinbase/wallet-sdk@3.9.3': resolution: {integrity: sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==} @@ -1558,11 +1387,6 @@ packages: peerDependencies: tailwindcss: ^3.0 || ^4.0 - '@hookform/resolvers@3.9.1': - resolution: {integrity: sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==} - peerDependencies: - react-hook-form: ^7.0.0 - '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} @@ -1913,11 +1737,102 @@ packages: resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==} engines: {node: '>=16.0.0'} + '@mui/core-downloads-tracker@7.3.6': + resolution: {integrity: sha512-QaYtTHlr8kDFN5mE1wbvVARRKH7Fdw1ZuOjBJcFdVpfNfRYKF3QLT4rt+WaB6CKJvpqxRsmEo0kpYinhH5GeHg==} + + '@mui/icons-material@7.3.6': + resolution: {integrity: sha512-0FfkXEj22ysIq5pa41A2NbcAhJSvmcZQ/vcTIbjDsd6hlslG82k5BEBqqS0ZJprxwIL3B45qpJ+bPHwJPlF7uQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@mui/material': ^7.3.6 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/material@7.3.6': + resolution: {integrity: sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@mui/material-pigment-css': ^7.3.6 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@mui/material-pigment-css': + optional: true + '@types/react': + optional: true + + '@mui/private-theming@7.3.6': + resolution: {integrity: sha512-Ws9wZpqM+FlnbZXaY/7yvyvWQo1+02Tbx50mVdNmzWEi51C51y56KAbaDCYyulOOBL6BJxuaqG8rNNuj7ivVyw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/styled-engine@7.3.6': + resolution: {integrity: sha512-+wiYbtvj+zyUkmDB+ysH6zRjuQIJ+CM56w0fEXV+VDNdvOuSywG+/8kpjddvvlfMLsaWdQe5oTuYGBcodmqGzQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + + '@mui/system@7.3.6': + resolution: {integrity: sha512-8fehAazkHNP1imMrdD2m2hbA9sl7Ur6jfuNweh5o4l9YPty4iaZzRXqYvBCWQNwFaSHmMEj2KPbyXGp7Bt73Rg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + + '@mui/types@7.4.9': + resolution: {integrity: sha512-dNO8Z9T2cujkSIaCnWwprfeKmTWh97cnjkgmpFJ2sbfXLx8SMZijCYHOtP/y5nnUb/Rm2omxbDMmtUoSaUtKaw==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/utils@7.3.6': + resolution: {integrity: sha512-jn+Ba02O6PiFs7nKva8R2aJJ9kJC+3kQ2R0BbKNY3KQQ36Qng98GnPRFTlbwYTdMD6hLEBKaMLUktyg/rTfd2w==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@napi-rs/wasm-runtime@1.0.3': resolution: {integrity: sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==} - '@next/bundle-analyzer@16.0.10': - resolution: {integrity: sha512-AHA6ZomhQuRsJtkoRvsq+hIuwA6F26mQzQT8ICcc2dL3BvHRcWOA+EiFr+BgWFY++EE957xVDqMIJjLApyxnwA==} + '@next/bundle-analyzer@16.1.1': + resolution: {integrity: sha512-aNJy301GGH8k36rDgrYdnyYEdjRQg6csMi1njzqHo+3qyZvOOWMHSv+p7SztNTzP5RU2KRwX0pPwYBtDcE+vVA==} '@next/env@16.0.10': resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==} @@ -2560,6 +2475,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + 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 + '@radix-ui/react-select@2.2.6': resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} peerDependencies: @@ -2595,6 +2523,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + 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 + '@radix-ui/react-use-callback-ref@1.1.1': resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: @@ -3082,10 +3023,6 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - '@sindresorhus/is@4.6.0': - resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} - engines: {node: '>=10'} - '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} @@ -3241,10 +3178,6 @@ packages: '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - '@szmarczak/http-timer@4.0.6': - resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} - engines: {node: '>=10'} - '@tanstack/query-core@5.8.3': resolution: {integrity: sha512-SWFMFtcHfttLYif6pevnnMYnBvxKf3C+MHMH7bevyYfpXpTMsLB9O6nNGBdWSoPwnZRXFNyNeVZOw25Wmdasow==} @@ -3292,12 +3225,6 @@ packages: '@types/react-dom': optional: true - '@testing-library/user-event@14.6.1': - resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@testing-library/dom': '>=7.21.4' - '@tootallnate/once@2.0.0': resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} @@ -3347,15 +3274,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/cacheable-request@6.0.3': - resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} - '@types/canvas-confetti@1.9.0': resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==} - '@types/chroma-js@2.4.5': - resolution: {integrity: sha512-6ISjhzJViaPCy2q2e6PgK+8HcHQDQ0V2LDiKmYAh+jJlLqDa6HbwDh0wOevHY0kHHUx0iZwjSRbVD47WOUx5EQ==} - '@types/connect@3.4.36': resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} @@ -3377,9 +3298,6 @@ packages: '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} - '@types/http-cache-semantics@4.0.4': - resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} - '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -3401,24 +3319,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/keyv@3.1.4': - resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - - '@types/lodash.mergewith@4.6.7': - resolution: {integrity: sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A==} - - '@types/lodash.mergewith@4.6.9': - resolution: {integrity: sha512-fgkoCAOF47K7sxrQ7Mlud2TH023itugZs2bUg8h/KzT+BnZNrR2jAOmaokbLunHNnobXVWOezAeNn/lZqwxkcw==} - - '@types/lodash@4.17.20': - resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} - '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/multicoin-address-validator@0.5.3': - resolution: {integrity: sha512-faImIjJkbXz6HdgZX4Hfr7GwuiEyGjcp49ugfu5rh8IhHNfaa5gNroQY4pARaaEgX1pgybVdc2iSc1h4B2fGMw==} - '@types/mysql@2.15.26': resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} @@ -3428,9 +3331,6 @@ packages: '@types/node@20.4.2': resolution: {integrity: sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==} - '@types/node@8.10.66': - resolution: {integrity: sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==} - '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -3446,20 +3346,19 @@ packages: '@types/pulltorefreshjs@0.1.7': resolution: {integrity: sha512-Y0g/yfuycIvpvUmP97n5NE2+HDAOwfREGVERjhMWw2Y0ODh5wvbflcQ5gXPZ+ihgoq+BQZjA1DL8apw2wAsJXA==} - '@types/react-csv@1.1.10': - resolution: {integrity: sha512-PESAyASL7Nfi/IyBR3ufd8qZkyoS+7jOylKmJxRZUZLFASLo4NZaRsJ8rNP8pCcbIziADyWBbLPD1nPddhsL4g==} - '@types/react-dom@18.3.7': resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} peerDependencies: '@types/react': ^18.0.0 + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + '@types/react@18.3.23': resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} - '@types/responselike@1.0.3': - resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} - '@types/shimmer@1.2.0': resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} @@ -3481,15 +3380,9 @@ packages: '@types/uuid@8.3.4': resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} - '@types/uuid@9.0.8': - resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} - '@types/validator@13.15.2': resolution: {integrity: sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==} - '@types/web-push@3.6.4': - resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==} - '@types/ws@7.4.7': resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} @@ -3733,15 +3626,6 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - '@zag-js/dom-query@0.31.1': - resolution: {integrity: sha512-oiuohEXAXhBxpzzNm9k2VHGEOLC1SXlXSbRPcfBZ9so5NRQUA++zCE7cyQJqGLTZR0t3itFLlZqDbYEXRrefwg==} - - '@zag-js/element-size@0.31.1': - resolution: {integrity: sha512-4T3yvn5NqqAjhlP326Fv+w9RqMIBbNN9H72g5q2ohwzhSgSfZzrKtjL4rs9axY/cw9UfMfXjRjEE98e5CMq7WQ==} - - '@zag-js/focus-visible@0.31.1': - resolution: {integrity: sha512-dbLksz7FEwyFoANbpIlNnd3bVm0clQSUsnP8yUVQucStZPsuWjCrhL2jlAbGNrTrahX96ntUMXHb/sM68TibFg==} - '@zerodev/passkey-validator@5.6.0': resolution: {integrity: sha512-ItnPs/6m3pT8tWaLqt31AFFQ4tAc5O01gtXP0Y7RW7xfuqUbgwYOghyeKMEkw6LfYykUWLhapL4B5c0CBYkgvg==} peerDependencies: @@ -3900,9 +3784,6 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - asn1.js@5.4.1: - resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} - ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -3920,11 +3801,6 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} - auto-text-size@0.2.3: - resolution: {integrity: sha512-OycpKD8n5jxnNaOiAJoNr3BkBsxRzHHhFQk15rgsOYq+O9lsMSPLbVN7ZibxjTwT7bl6Y5jawOtZJxRWaoxdcQ==} - peerDependencies: - react: '*' - autoprefixer@10.4.21: resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} engines: {node: ^10 || ^12 || >=14} @@ -4084,9 +3960,6 @@ packages: brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} - browserify-bignum@1.3.0-2: - resolution: {integrity: sha512-PwVvKC3WIV7ENfsG6VAIDq4R5st6kQt+Fod3WL5l7+MRONClo3J6xGQvRJHHM/ScwcNCH3GfYX5UOCuoNN/rLw==} - browserslist@4.25.2: resolution: {integrity: sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -4116,9 +3989,6 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - buffer-equal-constant-time@1.0.1: - resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -4129,21 +3999,10 @@ packages: resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} engines: {node: '>=6.14.2'} - bundle@2.1.0: - resolution: {integrity: sha512-d7TeT8m2HuymDjSEmMppWe/h5SSPPUZkaWKrAofx6gNXDdZ3FL/81oOTGPG+LIaZbNr9m4rtUi98Yw0Q1vHIIw==} - bytes-iec@3.1.1: resolution: {integrity: sha512-fey6+4jDK7TFtFg/klGSvNKJctyU7n2aQdnM+CO0ruLPbqqMOM8Tio0Pc+deqUeVKX1tL5DQep1zQ7+37aTAsA==} engines: {node: '>= 0.8'} - cacheable-lookup@5.0.4: - resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} - engines: {node: '>=10.6.0'} - - cacheable-request@7.0.4: - resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} - engines: {node: '>=8'} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -4188,18 +4047,6 @@ packages: canvas-confetti@1.9.3: resolution: {integrity: sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==} - cbor-js@0.1.0: - resolution: {integrity: sha512-7sQ/TvDZPl7csT1Sif9G0+MA0I0JOVah8+wWlJVQdVEgIbCzlN/ab3x+uvMNsc34TUvO6osQTAmB2ls80JX6tw==} - - chakra-ui-steps@2.2.0: - resolution: {integrity: sha512-yzug08HNmYFNcl2XTdBYM1ayPd527f/RiQYkIbW8l13q9sKk8foXKvrSSnDZkcHBMlEeXlN4ABKVsKtyb4e6Gw==} - peerDependencies: - '@chakra-ui/react': ^2.0.0 - '@emotion/react': ^11.4.1 - '@emotion/styled': ^11.3.0 - framer-motion: '>=4.0.0' - react: ^18.0.0 - chalk@3.0.0: resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} engines: {node: '>=8'} @@ -4268,9 +4115,6 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} - clone-response@1.0.3: - resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} - clsx@1.2.1: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} engines: {node: '>=6'} @@ -4293,9 +4137,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color2k@2.0.3: - resolution: {integrity: sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==} - colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -4353,9 +4194,6 @@ packages: cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} - copy-to-clipboard@3.3.3: - resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} - core-js-compat@3.45.1: resolution: {integrity: sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==} @@ -4371,15 +4209,6 @@ packages: engines: {node: '>=0.8'} hasBin: true - crc@4.3.2: - resolution: {integrity: sha512-uGDHf4KLLh2zsHa8D8hIQ1H/HtFQhyHrc0uhHBcoKGol/Xnb+MPYfUMw7cvON6ze/GUESTudKayDcJC5HnJv1A==} - engines: {node: '>=12'} - peerDependencies: - buffer: '>=6.0.3' - peerDependenciesMeta: - buffer: - optional: true - create-jest@29.7.0: resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4398,9 +4227,6 @@ packages: crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} - css-box-model@1.2.1: - resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} - css-color-keywords@1.0.0: resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} engines: {node: '>=4'} @@ -4579,10 +4405,6 @@ packages: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - dedent@1.6.0: resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} peerDependencies: @@ -4595,10 +4417,6 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - defer-to-connect@2.0.1: - resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} - engines: {node: '>=10'} - define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -4681,6 +4499,9 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + domexception@4.0.0: resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} engines: {node: '>=12'} @@ -4703,9 +4524,6 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ecdsa-sig-formatter@1.0.11: - resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - eciesjs@0.4.15: resolution: {integrity: sha512-r6kEJXDKecVOCj2nLMuXK/FCPeurW33+3JRpfXVbjLja3XUYFfD9I/JBreH6sUyzcm3G/YQboBjMla6poKeSdA==} engines: {bun: '>=1', deno: '>=2', node: '>=16'} @@ -5025,10 +4843,6 @@ packages: flow-enums-runtime@0.0.6: resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} - focus-lock@1.3.6: - resolution: {integrity: sha512-Ik/6OCk9RQQ0T5Xw+hKNLWrjSMtv51dD4GRmJjbD5a58TIEpI5a5iXagKVl3Z5UuyslMCA8Xwnu76jQob62Yhg==} - engines: {node: '>=10'} - follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -5079,9 +4893,6 @@ packages: react-dom: optional: true - framesync@6.1.2: - resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==} - fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -5163,10 +4974,6 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - got@11.8.6: - resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} - engines: {node: '>=10.19.0'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -5238,9 +5045,6 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hpagent@0.1.2: - resolution: {integrity: sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==} - html-encoding-sniffer@3.0.0: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} @@ -5248,9 +5052,6 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - http-cache-semantics@4.2.0: - resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -5263,14 +5064,6 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} - http2-wrapper@1.0.3: - resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} - engines: {node: '>=10.19.0'} - - http_ece@1.2.0: - resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==} - engines: {node: '>=16'} - https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -5665,9 +5458,6 @@ packages: js-sha3@0.8.0: resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} - js-sha512@0.9.0: - resolution: {integrity: sha512-mirki9WS/SUahm+1TbAPkqvbCiCfOAAsyXeHxK1UkullnJVVqoJG2pL9ObvT05CN+tM7fxhfYm0NbXn+1hWoZg==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -5701,9 +5491,6 @@ packages: engines: {node: '>=6'} hasBin: true - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -5728,15 +5515,6 @@ packages: jsqr@1.4.0: resolution: {integrity: sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==} - jssha@3.3.1: - resolution: {integrity: sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==} - - jwa@2.0.1: - resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - - jws@4.0.0: - resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} - kapsule@1.16.3: resolution: {integrity: sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==} engines: {node: '>=12'} @@ -5745,9 +5523,6 @@ packages: resolution: {integrity: sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==} engines: {node: '>=10.0.0'} - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - keyvaluestorage-interface@1.0.0: resolution: {integrity: sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==} @@ -5804,9 +5579,6 @@ packages: lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - lodash.clonedeep@4.5.0: - resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} - lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -5817,9 +5589,6 @@ packages: lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - lodash.mergewith@4.6.2: - resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} - lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} @@ -5833,19 +5602,6 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - lottie-react@2.4.1: - resolution: {integrity: sha512-LQrH7jlkigIIv++wIyrOYFLHSKQpEY4zehPicL9bQsrt1rnoKRYCYgpCUe5maqylNtacy58/sQDZTkwMcTRxZw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - lottie-web@5.13.0: - resolution: {integrity: sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==} - - lowercase-keys@2.0.0: - resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} - engines: {node: '>=8'} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -5980,14 +5736,6 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - mimic-response@1.0.1: - resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} - engines: {node: '>=4'} - - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -6055,10 +5803,6 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - multicoin-address-validator@0.5.26: - resolution: {integrity: sha512-WM9Zo7J+eVBa1tgzC/LZwaI1eu+FEwE+8X8KbEIIrb1Ghyi5tK38mzZdVp8PmIypZ1x4BHlNncWGOqvWw2/Cmg==} - engines: {node: '>=12.0.0'} - multiformats@9.9.0: resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} @@ -6118,11 +5862,6 @@ packages: sass: optional: true - ngrok@4.3.3: - resolution: {integrity: sha512-a2KApnkiG5urRxBPdDf76nNBQTnNNWXU0nXw0SsqsPI+Kmt2lGf9TdVYpYrHMnC+T9KhcNSWjCpWqBgC6QcFvw==} - engines: {node: '>=10.19.0 <14 || >=14.2'} - hasBin: true - node-addon-api@2.0.2: resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} @@ -6162,10 +5901,6 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} - normalize-url@6.1.0: - resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} - engines: {node: '>=10'} - npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -6239,10 +5974,6 @@ packages: oxc-resolver@11.6.1: resolution: {integrity: sha512-WQgmxevT4cM5MZ9ioQnEwJiHpPzbvntV5nInGAKo9NQZzegcOonHvcVcnkYqld7bTG35UFHEKeF7VwwsmA3cZg==} - p-cancelable@2.1.1: - resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} - engines: {node: '>=8'} - p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -6551,9 +6282,6 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - property-expr@2.0.6: - resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} - proxy-agent@6.5.0: resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} engines: {node: '>= 14'} @@ -6617,10 +6345,6 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} - quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} - radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -6631,14 +6355,6 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - react-clientside-effect@1.2.8: - resolution: {integrity: sha512-ma2FePH0z3px2+WOu6h+YycZcEvFmmxIlAb62cF52bG86eMySciO/EQZeQMXd07kPCYB0a1dWDT5J+KE9mCDUw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - - react-csv@2.2.2: - resolution: {integrity: sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw==} - react-devtools-core@6.1.5: resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} @@ -6647,33 +6363,18 @@ packages: peerDependencies: react: ^19.2.1 - react-fast-compare@3.2.2: - resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - react-fast-marquee@1.6.5: resolution: {integrity: sha512-swDnPqrT2XISAih0o74zQVE2wQJFMvkx+9VZXYYNSLb/CUcAzU9pNj637Ar2+hyRw6b4tP6xh4GQZip2ZCpQpg==} peerDependencies: react: '>= 16.8.0 || ^18.0.0' react-dom: '>= 16.8.0 || ^18.0.0' - react-focus-lock@2.13.6: - resolution: {integrity: sha512-ehylFFWyYtBKXjAO9+3v8d0i+cnc1trGS0vlTGhzFW1vbFXVUTmR8s2tt/ZQG8x5hElg6rhENlLG1H3EZK0Llg==} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - react-force-graph-2d@1.29.0: resolution: {integrity: sha512-Xv5IIk+hsZmB3F2ibja/t6j/b0/1T9dtFOQacTUoLpgzRHrO6wPu1GtQ2LfRqI/imgtaapnXUgQaE8g8enPo5w==} engines: {node: '>=12'} peerDependencies: react: '*' - react-ga4@2.1.0: - resolution: {integrity: sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==} - react-hook-form@7.62.0: resolution: {integrity: sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==} engines: {node: '>=18.0.0'} @@ -6689,6 +6390,9 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-is@19.2.3: + resolution: {integrity: sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==} + react-kapsule@2.5.7: resolution: {integrity: sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==} engines: {node: '>=12'} @@ -6767,6 +6471,12 @@ packages: react: '>=16.14.0' react-dom: '>=16.14.0' + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + react-transition-state@1.1.5: resolution: {integrity: sha512-ITY2mZqc2dWG2eitJkYNdcSFW8aKeOlkL2A/vowRrLL8GH3J6Re/SpD/BLvQzrVOTqjsP0b5S9N10vgNNzwMUQ==} peerDependencies: @@ -6865,9 +6575,6 @@ packages: resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} - resolve-alpn@1.2.1: - resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -6896,9 +6603,6 @@ packages: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true - responselike@2.0.1: - resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -7366,12 +7070,6 @@ packages: throat@5.0.0: resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} - tiny-case@1.0.3: - resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} - - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} @@ -7390,16 +7088,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - toggle-selection@1.0.6: - resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} - toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - toposort@2.0.2: - resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -7454,9 +7146,6 @@ packages: tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - tslib@2.4.0: - resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -7477,10 +7166,6 @@ packages: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} - type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} - type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} @@ -7664,10 +7349,6 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - uuid@10.0.0: - resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} - hasBin: true - uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -7742,11 +7423,6 @@ packages: resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} engines: {node: '>=10.13.0'} - web-push@3.6.7: - resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==} - engines: {node: '>= 16'} - hasBin: true - webextension-polyfill@0.10.0: resolution: {integrity: sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==} @@ -7965,9 +7641,6 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yup@1.7.0: - resolution: {integrity: sha512-VJce62dBd+JQvoc+fCVq+KZfPHr+hXaxCcVgotfwWvlR0Ja3ffYKaJBT8rptPOSKOGJDCUnW2C2JWpud7aRP6Q==} - zod-validation-error@3.5.3: resolution: {integrity: sha512-OT5Y8lbUadqVZCsnyFaTQ4/O2mys4tj7PqhdbBCp7McPwvIEKfPtdA6QfPeFQK2/Rz5LgwmAXRJTugBNBi0btw==} engines: {node: '>=18.0.0'} @@ -8633,160 +8306,7 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@calcom/embed-core@1.5.3': {} - - '@calcom/embed-react@1.5.3(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': - dependencies: - '@calcom/embed-core': 1.5.3 - '@calcom/embed-snippet': 1.3.3 - react: 19.2.1 - react-dom: 19.2.1(react@19.2.1) - - '@calcom/embed-snippet@1.3.3': - dependencies: - '@calcom/embed-core': 1.5.3 - - '@chakra-ui/anatomy@2.2.2': {} - - '@chakra-ui/anatomy@2.3.6': {} - - '@chakra-ui/color-mode@2.2.0(react@19.2.1)': - dependencies: - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@19.2.1) - react: 19.2.1 - - '@chakra-ui/hooks@2.4.5(react@19.2.1)': - dependencies: - '@chakra-ui/utils': 2.2.5(react@19.2.1) - '@zag-js/element-size': 0.31.1 - copy-to-clipboard: 3.3.3 - framesync: 6.1.2 - react: 19.2.1 - - '@chakra-ui/icon@3.2.0(@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(react@19.2.1))(react@19.2.1)': - dependencies: - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(react@19.2.1) - react: 19.2.1 - - '@chakra-ui/object-utils@2.1.0': {} - - '@chakra-ui/react-context@2.1.0(react@19.2.1)': - dependencies: - react: 19.2.1 - - '@chakra-ui/react-use-safe-layout-effect@2.1.0(react@19.2.1)': - dependencies: - react: 19.2.1 - - '@chakra-ui/react-utils@2.0.12(react@19.2.1)': - dependencies: - '@chakra-ui/utils': 2.0.15 - react: 19.2.1 - - '@chakra-ui/react@2.10.9(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': - dependencies: - '@chakra-ui/hooks': 2.4.5(react@19.2.1) - '@chakra-ui/styled-system': 2.12.4(react@19.2.1) - '@chakra-ui/theme': 3.4.9(@chakra-ui/styled-system@2.12.4(react@19.2.1))(react@19.2.1) - '@chakra-ui/utils': 2.2.5(react@19.2.1) - '@emotion/react': 11.14.0(@types/react@18.3.23)(react@19.2.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1) - '@popperjs/core': 2.11.8 - '@zag-js/focus-visible': 0.31.1 - aria-hidden: 1.2.6 - framer-motion: 11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) - react: 19.2.1 - react-dom: 19.2.1(react@19.2.1) - react-fast-compare: 3.2.2 - react-focus-lock: 2.13.6(@types/react@18.3.23)(react@19.2.1) - react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@19.2.1) - transitivePeerDependencies: - - '@types/react' - - '@chakra-ui/shared-utils@2.0.4': {} - - '@chakra-ui/shared-utils@2.0.5': {} - - '@chakra-ui/styled-system@2.12.4(react@19.2.1)': - dependencies: - '@chakra-ui/utils': 2.2.5(react@19.2.1) - csstype: 3.1.3 - transitivePeerDependencies: - - react - - '@chakra-ui/styled-system@2.9.2': - dependencies: - '@chakra-ui/shared-utils': 2.0.5 - csstype: 3.1.3 - lodash.mergewith: 4.6.2 - - '@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(react@19.2.1)': - dependencies: - '@chakra-ui/color-mode': 2.2.0(react@19.2.1) - '@chakra-ui/object-utils': 2.1.0 - '@chakra-ui/react-utils': 2.0.12(react@19.2.1) - '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/theme-utils': 2.0.21 - '@chakra-ui/utils': 2.0.15 - '@emotion/react': 11.14.0(@types/react@18.3.23)(react@19.2.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1) - react: 19.2.1 - react-fast-compare: 3.2.2 - - '@chakra-ui/theme-tools@2.1.2(@chakra-ui/styled-system@2.9.2)': - dependencies: - '@chakra-ui/anatomy': 2.2.2 - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/styled-system': 2.9.2 - color2k: 2.0.3 - - '@chakra-ui/theme-tools@2.2.9(@chakra-ui/styled-system@2.12.4(react@19.2.1))(react@19.2.1)': - dependencies: - '@chakra-ui/anatomy': 2.3.6 - '@chakra-ui/styled-system': 2.12.4(react@19.2.1) - '@chakra-ui/utils': 2.2.5(react@19.2.1) - color2k: 2.0.3 - transitivePeerDependencies: - - react - - '@chakra-ui/theme-utils@2.0.21': - dependencies: - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) - lodash.mergewith: 4.6.2 - - '@chakra-ui/theme@3.3.1(@chakra-ui/styled-system@2.9.2)': - dependencies: - '@chakra-ui/anatomy': 2.2.2 - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/theme-tools': 2.1.2(@chakra-ui/styled-system@2.9.2) - - '@chakra-ui/theme@3.4.9(@chakra-ui/styled-system@2.12.4(react@19.2.1))(react@19.2.1)': - dependencies: - '@chakra-ui/anatomy': 2.3.6 - '@chakra-ui/styled-system': 2.12.4(react@19.2.1) - '@chakra-ui/theme-tools': 2.2.9(@chakra-ui/styled-system@2.12.4(react@19.2.1))(react@19.2.1) - '@chakra-ui/utils': 2.2.5(react@19.2.1) - transitivePeerDependencies: - - react - - '@chakra-ui/utils@2.0.15': - dependencies: - '@types/lodash.mergewith': 4.6.7 - css-box-model: 1.2.1 - framesync: 6.1.2 - lodash.mergewith: 4.6.2 - - '@chakra-ui/utils@2.2.5(react@19.2.1)': - dependencies: - '@types/lodash.mergewith': 4.6.9 - lodash.mergewith: 4.6.2 - react: 19.2.1 - - '@coinbase/wallet-sdk@3.9.3': + '@coinbase/wallet-sdk@3.9.3': dependencies: bn.js: 5.2.2 buffer: 6.0.3 @@ -8851,13 +8371,13 @@ snapshots: - typescript - utf-8-validate - '@daimo/pay@1.16.5(05566c87f67e0ead90f6e2b4ff43e130)': + '@daimo/pay@1.16.5(e6cc8f89f2a748207ee80ab4acbea829)': dependencies: '@daimo/pay-common': 1.16.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) '@solana/wallet-adapter-base': 0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)) - '@solana/wallet-adapter-react': 0.15.39(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(bs58@5.0.0)(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) + '@solana/wallet-adapter-react': 0.15.39(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(bs58@5.0.0)(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@tanstack/react-query': 5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) + '@tanstack/react-query': 5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) '@trpc/client': 11.5.0(@trpc/server@11.5.0(typescript@5.9.2))(typescript@5.9.2) '@trpc/server': 11.5.0(typescript@5.9.2) buffer: 6.0.3 @@ -8871,9 +8391,9 @@ snapshots: react-transition-state: 1.1.5(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react-use-measure: 2.1.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1) resize-observer-polyfill: 1.5.1 - styled-components: 5.3.11(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react-is@18.3.1)(react@19.2.1) + styled-components: 5.3.11(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react-is@19.2.3)(react@19.2.1) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - wagmi: 2.16.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.8.3)(@tanstack/react-query@5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + wagmi: 2.16.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.8.3)(@tanstack/react-query@5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) transitivePeerDependencies: - '@babel/core' - '@emotion/is-prop-valid' @@ -9102,6 +8622,7 @@ snapshots: '@emotion/is-prop-valid@1.4.0': dependencies: '@emotion/memoize': 0.9.0 + optional: true '@emotion/memoize@0.9.0': {} @@ -9145,6 +8666,7 @@ snapshots: '@types/react': 18.3.23 transitivePeerDependencies: - supports-color + optional: true '@emotion/stylis@0.8.5': {} @@ -9611,10 +9133,6 @@ snapshots: dependencies: tailwindcss: 3.4.17 - '@hookform/resolvers@3.9.1(react-hook-form@7.62.0(react@19.2.1))': - dependencies: - react-hook-form: 7.62.0(react@19.2.1) - '@img/colour@1.0.0': optional: true @@ -9923,17 +9441,17 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@justaname.id/react@0.3.180(3309f1e2f8bb0def69834732f15d7315)': + '@justaname.id/react@0.3.180(f21193980fd875d23a1e8d26c751ddf1)': dependencies: '@ensdomains/ensjs': 4.0.2(encoding@0.1.13)(typescript@5.9.2)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) '@justaname.id/sdk': 0.2.177(ethers@5.7.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(siwe@2.3.2(ethers@5.7.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@tanstack/react-query': 5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) + '@tanstack/react-query': 5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) axios: 1.11.0 ethers: 5.7.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) qs: 6.14.0 react: 19.2.1 viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - wagmi: 2.16.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.8.3)(@tanstack/react-query@5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + wagmi: 2.16.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.8.3)(@tanstack/react-query@5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) transitivePeerDependencies: - debug - encoding @@ -10063,7 +9581,7 @@ snapshots: '@metamask/sdk@0.32.0(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 '@metamask/onboarding': 1.0.1 '@metamask/providers': 16.1.0 '@metamask/sdk-communication-layer': 0.32.0(cross-fetch@4.1.0(encoding@0.1.13))(eciesjs@0.4.15)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -10143,6 +9661,93 @@ snapshots: transitivePeerDependencies: - supports-color + '@mui/core-downloads-tracker@7.3.6': {} + + '@mui/icons-material@7.3.6(@mui/material@7.3.6(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(@types/react@18.3.23)(react@19.2.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/material': 7.3.6(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + react: 19.2.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@mui/material@7.3.6(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/core-downloads-tracker': 7.3.6 + '@mui/system': 7.3.6(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1) + '@mui/types': 7.4.9(@types/react@18.3.23) + '@mui/utils': 7.3.6(@types/react@18.3.23)(react@19.2.1) + '@popperjs/core': 2.11.8 + '@types/react-transition-group': 4.4.12(@types/react@18.3.23) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + react-is: 19.2.3 + react-transition-group: 4.4.5(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@18.3.23)(react@19.2.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1) + '@types/react': 18.3.23 + + '@mui/private-theming@7.3.6(@types/react@18.3.23)(react@19.2.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/utils': 7.3.6(@types/react@18.3.23)(react@19.2.1) + prop-types: 15.8.1 + react: 19.2.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@mui/styled-engine@7.3.6(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(react@19.2.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/sheet': 1.4.0 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.2.1 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@18.3.23)(react@19.2.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1) + + '@mui/system@7.3.6(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/private-theming': 7.3.6(@types/react@18.3.23)(react@19.2.1) + '@mui/styled-engine': 7.3.6(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(react@19.2.1) + '@mui/types': 7.4.9(@types/react@18.3.23) + '@mui/utils': 7.3.6(@types/react@18.3.23)(react@19.2.1) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.2.1 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@18.3.23)(react@19.2.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1) + '@types/react': 18.3.23 + + '@mui/types@7.4.9(@types/react@18.3.23)': + dependencies: + '@babel/runtime': 7.28.4 + optionalDependencies: + '@types/react': 18.3.23 + + '@mui/utils@7.3.6(@types/react@18.3.23)(react@19.2.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/types': 7.4.9(@types/react@18.3.23) + '@types/prop-types': 15.7.15 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.2.1 + react-is: 19.2.3 + optionalDependencies: + '@types/react': 18.3.23 + '@napi-rs/wasm-runtime@1.0.3': dependencies: '@emnapi/core': 1.4.5 @@ -10150,7 +9755,7 @@ snapshots: '@tybys/wasm-util': 0.10.0 optional: true - '@next/bundle-analyzer@16.0.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@next/bundle-analyzer@16.1.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: webpack-bundle-analyzer: 4.10.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: @@ -10791,6 +10396,23 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@19.2.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@19.2.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@19.2.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@19.2.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-select@2.2.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/number': 1.1.1 @@ -10846,6 +10468,22 @@ snapshots: optionalDependencies: '@types/react': 18.3.23 + '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@19.2.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@19.2.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@19.2.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.23)(react@19.2.1)': dependencies: react: 19.2.1 @@ -10947,10 +10585,10 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) - '@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))': + '@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))': dependencies: merge-options: 3.0.4 - react-native: 0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10) + react-native: 0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10) optional: true '@react-native/assets-registry@0.81.0': {} @@ -11022,10 +10660,10 @@ snapshots: nullthrows: 1.1.1 yargs: 17.7.2 - '@react-native/community-cli-plugin@0.81.0(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@react-native/community-cli-plugin@0.81.0(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@react-native/dev-middleware': 0.81.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@react-native/metro-config': 0.81.0(@babel/core@7.28.3) + '@react-native/metro-config': 0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10) debug: 4.4.3 invariant: 2.2.4 metro: 0.83.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -11070,7 +10708,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@react-native/metro-config@0.81.0(@babel/core@7.28.3)': + '@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@react-native/js-polyfills': 0.81.0 '@react-native/metro-babel-transformer': 0.81.0(@babel/core@7.28.3) @@ -11078,16 +10716,18 @@ snapshots: metro-runtime: 0.83.2 transitivePeerDependencies: - '@babel/core' + - bufferutil - supports-color + - utf-8-validate '@react-native/normalize-colors@0.81.0': {} - '@react-native/virtualized-lists@0.81.0(@types/react@18.3.23)(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1)': + '@react-native/virtualized-lists@0.81.0(@types/react@18.3.23)(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 react: 19.2.1 - react-native: 0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10) + react-native: 0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10) optionalDependencies: '@types/react': 18.3.23 @@ -11116,24 +10756,24 @@ snapshots: react: 19.2.1 react-redux: 9.2.0(@types/react@18.3.23)(react@19.2.1)(redux@5.0.1) - '@reown/appkit-adapter-wagmi@1.6.9(1120c5d6420457d1541c4aa99b99d00c)': + '@reown/appkit-adapter-wagmi@1.6.9(3a722632bd268f29b4a2886282b3288f)': dependencies: - '@reown/appkit': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-common': 1.6.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-core': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-core': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.6.9 - '@reown/appkit-scaffold-ui': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) + '@reown/appkit-scaffold-ui': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) '@reown/appkit-ui': 1.6.9 - '@reown/appkit-utils': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) + '@reown/appkit-utils': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) '@reown/appkit-wallet': 1.6.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) '@wagmi/core': 2.19.0(@tanstack/query-core@5.8.3)(@types/react@18.3.23)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@walletconnect/universal-provider': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) - '@walletconnect/utils': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/universal-provider': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@walletconnect/utils': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) valtio: 1.13.2(@types/react@18.3.23)(react@19.2.1) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - wagmi: 2.16.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.8.3)(@tanstack/react-query@5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + wagmi: 2.16.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.8.3)(@tanstack/react-query@5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) optionalDependencies: - '@wagmi/connectors': 5.9.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(@wagmi/core@2.19.0(@tanstack/query-core@5.8.3)(@types/react@18.3.23)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + '@wagmi/connectors': 5.9.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(@wagmi/core@2.19.0(@tanstack/query-core@5.8.3)(@types/react@18.3.23)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -11208,11 +10848,11 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-controllers@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-controllers@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@18.3.23)(react@19.2.1) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: @@ -11242,11 +10882,11 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-core@1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-core@1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.6.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-wallet': 1.6.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) valtio: 1.13.2(@types/react@18.3.23)(react@19.2.1) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: @@ -11276,12 +10916,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-pay@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) lit: 3.3.0 valtio: 1.13.2(@types/react@18.3.23)(react@19.2.1) transitivePeerDependencies: @@ -11319,12 +10959,12 @@ snapshots: dependencies: buffer: 6.0.3 - '@reown/appkit-scaffold-ui@1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76)': + '@reown/appkit-scaffold-ui@1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.6.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-core': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-core': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-ui': 1.6.9 - '@reown/appkit-utils': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) + '@reown/appkit-utils': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) '@reown/appkit-wallet': 1.6.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) lit: 3.1.0 transitivePeerDependencies: @@ -11355,12 +10995,12 @@ snapshots: - valtio - zod - '@reown/appkit-scaffold-ui@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76)': + '@reown/appkit-scaffold-ui@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -11396,10 +11036,10 @@ snapshots: lit: 3.1.0 qrcode: 1.5.3 - '@reown/appkit-ui@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-ui@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 @@ -11430,14 +11070,14 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76)': + '@reown/appkit-utils@1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.6.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-core': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-core': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.6.9 '@reown/appkit-wallet': 1.6.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) '@walletconnect/logger': 2.1.2 - '@walletconnect/universal-provider': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) valtio: 1.13.2(@types/react@18.3.23)(react@19.2.1) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: @@ -11467,14 +11107,14 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76)': + '@reown/appkit-utils@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) '@walletconnect/logger': 2.1.2 - '@walletconnect/universal-provider': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@18.3.23)(react@19.2.1) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: @@ -11526,18 +11166,18 @@ snapshots: - typescript - utf-8-validate - '@reown/appkit@1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit@1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.6.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-core': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-core': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.6.9 - '@reown/appkit-scaffold-ui': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) + '@reown/appkit-scaffold-ui': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) '@reown/appkit-ui': 1.6.9 - '@reown/appkit-utils': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) + '@reown/appkit-utils': 1.6.9(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) '@reown/appkit-wallet': 1.6.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@walletconnect/types': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) - '@walletconnect/universal-provider': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) - '@walletconnect/utils': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/types': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/universal-provider': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@walletconnect/utils': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) bs58: 6.0.0 valtio: 1.13.2(@types/react@18.3.23)(react@19.2.1) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) @@ -11568,18 +11208,18 @@ snapshots: - utf-8-validate - zod - '@reown/appkit@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-pay': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-scaffold-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) + '@reown/appkit-scaffold-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.23)(react@19.2.1))(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@walletconnect/types': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) - '@walletconnect/universal-provider': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/universal-provider': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) bs58: 6.0.0 valtio: 1.13.2(@types/react@18.3.23)(react@19.2.1) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) @@ -11912,8 +11552,6 @@ snapshots: '@sinclair/typebox@0.27.8': {} - '@sindresorhus/is@4.6.0': {} - '@sinonjs/commons@3.0.1': dependencies: type-detect: 4.0.8 @@ -11955,9 +11593,9 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} - '@solana-mobile/mobile-wallet-adapter-protocol-web3js@2.2.3(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1)': + '@solana-mobile/mobile-wallet-adapter-protocol-web3js@2.2.3(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1)': dependencies: - '@solana-mobile/mobile-wallet-adapter-protocol': 2.2.3(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(bs58@5.0.0)(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) + '@solana-mobile/mobile-wallet-adapter-protocol': 2.2.3(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(bs58@5.0.0)(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10) bs58: 5.0.0 js-base64: 3.7.8 @@ -11966,36 +11604,36 @@ snapshots: - react - react-native - '@solana-mobile/mobile-wallet-adapter-protocol@2.2.3(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(bs58@5.0.0)(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1)': + '@solana-mobile/mobile-wallet-adapter-protocol@2.2.3(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(bs58@5.0.0)(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1)': dependencies: '@solana/wallet-standard': 1.1.4(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(bs58@5.0.0)(react@19.2.1) '@solana/wallet-standard-util': 1.1.2 '@wallet-standard/core': 1.1.1 js-base64: 3.7.8 - react-native: 0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10) + react-native: 0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10) transitivePeerDependencies: - '@solana/wallet-adapter-base' - '@solana/web3.js' - bs58 - react - '@solana-mobile/wallet-adapter-mobile@2.2.3(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1)': + '@solana-mobile/wallet-adapter-mobile@2.2.3(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1)': dependencies: - '@solana-mobile/mobile-wallet-adapter-protocol-web3js': 2.2.3(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) - '@solana-mobile/wallet-standard-mobile': 0.4.1(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) + '@solana-mobile/mobile-wallet-adapter-protocol-web3js': 2.2.3(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) + '@solana-mobile/wallet-standard-mobile': 0.4.1(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) '@solana/wallet-adapter-base': 0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)) '@solana/wallet-standard-features': 1.3.0 '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10) js-base64: 3.7.8 optionalDependencies: - '@react-native-async-storage/async-storage': 1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)) + '@react-native-async-storage/async-storage': 1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)) transitivePeerDependencies: - react - react-native - '@solana-mobile/wallet-standard-mobile@0.4.1(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1)': + '@solana-mobile/wallet-standard-mobile@0.4.1(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1)': dependencies: - '@solana-mobile/mobile-wallet-adapter-protocol': 2.2.3(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(bs58@5.0.0)(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) + '@solana-mobile/mobile-wallet-adapter-protocol': 2.2.3(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(bs58@5.0.0)(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) '@solana/wallet-standard-chains': 1.1.1 '@solana/wallet-standard-features': 1.3.0 '@wallet-standard/base': 1.1.0 @@ -12038,9 +11676,9 @@ snapshots: '@wallet-standard/features': 1.1.0 eventemitter3: 5.0.1 - '@solana/wallet-adapter-react@0.15.39(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(bs58@5.0.0)(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1)': + '@solana/wallet-adapter-react@0.15.39(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(bs58@5.0.0)(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1)': dependencies: - '@solana-mobile/wallet-adapter-mobile': 2.2.3(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) + '@solana-mobile/wallet-adapter-mobile': 2.2.3(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) '@solana/wallet-adapter-base': 0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)) '@solana/wallet-standard-wallet-adapter-react': 1.1.4(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(bs58@5.0.0)(react@19.2.1) '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10) @@ -12116,7 +11754,7 @@ snapshots: '@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@solana/buffer-layout': 4.0.1 @@ -12176,19 +11814,15 @@ snapshots: dependencies: tslib: 2.8.1 - '@szmarczak/http-timer@4.0.6': - dependencies: - defer-to-connect: 2.0.1 - '@tanstack/query-core@5.8.3': {} - '@tanstack/react-query@5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1)': + '@tanstack/react-query@5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1)': dependencies: '@tanstack/query-core': 5.8.3 react: 19.2.1 optionalDependencies: react-dom: 19.2.1(react@19.2.1) - react-native: 0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10) + react-native: 0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10) '@tanstack/react-virtual@3.13.12(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: @@ -12228,10 +11862,6 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': - dependencies: - '@testing-library/dom': 10.4.1 - '@tootallnate/once@2.0.0': {} '@tootallnate/quickjs-emscripten@0.23.0': {} @@ -12283,17 +11913,8 @@ snapshots: dependencies: '@babel/types': 7.28.2 - '@types/cacheable-request@6.0.3': - dependencies: - '@types/http-cache-semantics': 4.0.4 - '@types/keyv': 3.1.4 - '@types/node': 20.4.2 - '@types/responselike': 1.0.3 - '@types/canvas-confetti@1.9.0': {} - '@types/chroma-js@2.4.5': {} - '@types/connect@3.4.36': dependencies: '@types/node': 20.4.2 @@ -12322,8 +11943,6 @@ snapshots: dependencies: '@types/node': 20.4.2 - '@types/http-cache-semantics@4.0.4': {} - '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -12349,24 +11968,8 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/keyv@3.1.4': - dependencies: - '@types/node': 20.4.2 - - '@types/lodash.mergewith@4.6.7': - dependencies: - '@types/lodash': 4.17.20 - - '@types/lodash.mergewith@4.6.9': - dependencies: - '@types/lodash': 4.17.20 - - '@types/lodash@4.17.20': {} - '@types/ms@2.1.0': {} - '@types/multicoin-address-validator@0.5.3': {} - '@types/mysql@2.15.26': dependencies: '@types/node': 20.4.2 @@ -12375,8 +11978,6 @@ snapshots: '@types/node@20.4.2': {} - '@types/node@8.10.66': {} - '@types/parse-json@4.0.2': {} '@types/pg-pool@2.0.6': @@ -12393,11 +11994,11 @@ snapshots: '@types/pulltorefreshjs@0.1.7': {} - '@types/react-csv@1.1.10': + '@types/react-dom@18.3.7(@types/react@18.3.23)': dependencies: '@types/react': 18.3.23 - '@types/react-dom@18.3.7(@types/react@18.3.23)': + '@types/react-transition-group@4.4.12(@types/react@18.3.23)': dependencies: '@types/react': 18.3.23 @@ -12406,10 +12007,6 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.1.3 - '@types/responselike@1.0.3': - dependencies: - '@types/node': 20.4.2 - '@types/shimmer@1.2.0': {} '@types/stack-utils@2.0.3': {} @@ -12426,14 +12023,8 @@ snapshots: '@types/uuid@8.3.4': {} - '@types/uuid@9.0.8': {} - '@types/validator@13.15.2': {} - '@types/web-push@3.6.4': - dependencies: - '@types/node': 20.4.2 - '@types/ws@7.4.7': dependencies: '@types/node': 20.4.2 @@ -12458,7 +12049,7 @@ snapshots: next: 16.0.10(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 19.2.1 - '@wagmi/connectors@5.9.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(@wagmi/core@2.19.0(@tanstack/query-core@5.8.3)(@types/react@18.3.23)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)': + '@wagmi/connectors@5.9.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(@wagmi/core@2.19.0(@tanstack/query-core@5.8.3)(@types/react@18.3.23)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)': dependencies: '@base-org/account': 1.1.1(@types/react@18.3.23)(bufferutil@4.0.9)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) '@coinbase/wallet-sdk': 4.3.6(@types/react@18.3.23)(bufferutil@4.0.9)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) @@ -12467,7 +12058,7 @@ snapshots: '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@wagmi/core': 2.19.0(@tanstack/query-core@5.8.3)(@types/react@18.3.23)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@walletconnect/ethereum-provider': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/ethereum-provider': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: @@ -12501,7 +12092,7 @@ snapshots: - utf-8-validate - zod - '@wagmi/connectors@5.9.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(@wagmi/core@2.19.0(@tanstack/query-core@5.8.3)(@types/react@18.3.23)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)': + '@wagmi/connectors@5.9.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(@wagmi/core@2.19.0(@tanstack/query-core@5.8.3)(@types/react@18.3.23)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)': dependencies: '@base-org/account': 1.1.1(@types/react@18.3.23)(bufferutil@4.0.9)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) '@coinbase/wallet-sdk': 4.3.6(@types/react@18.3.23)(bufferutil@4.0.9)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) @@ -12510,7 +12101,7 @@ snapshots: '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@wagmi/core': 2.19.0(@tanstack/query-core@5.8.3)(@types/react@18.3.23)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@walletconnect/ethereum-provider': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/ethereum-provider': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: @@ -12602,21 +12193,21 @@ snapshots: dependencies: '@wallet-standard/base': 1.1.0 - '@walletconnect/core@2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@walletconnect/core@2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/types': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/window-getters': 1.0.1 events: 3.3.0 lodash.isequal: 4.5.0 @@ -12643,21 +12234,21 @@ snapshots: - uploadthing - utf-8-validate - '@walletconnect/core@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/core@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.33.0 events: 3.3.0 @@ -12686,21 +12277,21 @@ snapshots: - utf-8-validate - zod - '@walletconnect/core@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/core@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.33.0 events: 3.3.0 @@ -12733,18 +12324,18 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/ethereum-provider@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/ethereum-provider@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/jsonrpc-http-connection': 1.0.8(encoding@0.1.13) '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) - '@walletconnect/sign-client': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@walletconnect/types': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) - '@walletconnect/universal-provider': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/sign-client': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/universal-provider': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -12820,13 +12411,13 @@ snapshots: - bufferutil - utf-8-validate - '@walletconnect/keyvaluestorage@1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))': + '@walletconnect/keyvaluestorage@1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))': dependencies: '@walletconnect/safe-json': 1.0.2 idb-keyval: 6.2.2 unstorage: 1.16.1(idb-keyval@6.2.2) optionalDependencies: - '@react-native-async-storage/async-storage': 1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)) + '@react-native-async-storage/async-storage': 1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -12867,16 +12458,16 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/sign-client@2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@walletconnect/sign-client@2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: - '@walletconnect/core': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@walletconnect/core': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/types': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -12900,16 +12491,16 @@ snapshots: - uploadthing - utf-8-validate - '@walletconnect/sign-client@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/sign-client@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@walletconnect/core': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/core': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -12935,16 +12526,16 @@ snapshots: - utf-8-validate - zod - '@walletconnect/sign-client@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/sign-client@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@walletconnect/core': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/core': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -12974,12 +12565,12 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/types@2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))': + '@walletconnect/types@2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-types': 1.0.4 - '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 events: 3.3.0 transitivePeerDependencies: @@ -13002,12 +12593,12 @@ snapshots: - ioredis - uploadthing - '@walletconnect/types@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))': + '@walletconnect/types@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-types': 1.0.4 - '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 events: 3.3.0 transitivePeerDependencies: @@ -13030,12 +12621,12 @@ snapshots: - ioredis - uploadthing - '@walletconnect/types@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))': + '@walletconnect/types@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-types': 1.0.4 - '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 events: 3.3.0 transitivePeerDependencies: @@ -13058,18 +12649,18 @@ snapshots: - ioredis - uploadthing - '@walletconnect/universal-provider@2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10)': + '@walletconnect/universal-provider@2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8(encoding@0.1.13) '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@walletconnect/types': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/sign-client': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@walletconnect/types': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) events: 3.3.0 lodash: 4.17.21 transitivePeerDependencies: @@ -13095,18 +12686,18 @@ snapshots: - uploadthing - utf-8-validate - '@walletconnect/universal-provider@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/universal-provider@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8(encoding@0.1.13) '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@walletconnect/types': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/sign-client': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) es-toolkit: 1.33.0 events: 3.3.0 transitivePeerDependencies: @@ -13134,18 +12725,18 @@ snapshots: - utf-8-validate - zod - '@walletconnect/universal-provider@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/universal-provider@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8(encoding@0.1.13) '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@walletconnect/types': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/sign-client': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) es-toolkit: 1.33.0 events: 3.3.0 transitivePeerDependencies: @@ -13173,19 +12764,19 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))': + '@walletconnect/utils@2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))': dependencies: '@ethersproject/transactions': 5.7.0 '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 '@noble/hashes': 1.7.1 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/types': 2.18.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/window-getters': 1.0.1 '@walletconnect/window-metadata': 1.0.1 detect-browser: 5.3.0 @@ -13212,18 +12803,18 @@ snapshots: - ioredis - uploadthing - '@walletconnect/utils@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/utils@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 '@noble/hashes': 1.7.1 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/types': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/window-getters': 1.0.1 '@walletconnect/window-metadata': 1.0.1 bs58: 6.0.0 @@ -13255,18 +12846,18 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/utils@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 '@noble/hashes': 1.7.1 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) + '@walletconnect/types': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))) '@walletconnect/window-getters': 1.0.1 '@walletconnect/window-metadata': 1.0.1 bs58: 6.0.0 @@ -13387,14 +12978,6 @@ snapshots: '@xtuc/long@4.2.2': {} - '@zag-js/dom-query@0.31.1': {} - - '@zag-js/element-size@0.31.1': {} - - '@zag-js/focus-visible@0.31.1': - dependencies: - '@zag-js/dom-query': 0.31.1 - '@zerodev/passkey-validator@5.6.0(@zerodev/sdk@5.5.0(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(@zerodev/webauthn-key@5.4.4(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@noble/curves': 1.9.7 @@ -13534,13 +13117,6 @@ snapshots: asap@2.0.6: {} - asn1.js@5.4.1: - dependencies: - bn.js: 4.12.2 - inherits: 2.0.4 - minimalistic-assert: 1.0.1 - safer-buffer: 2.1.2 - ast-types@0.13.4: dependencies: tslib: 2.8.1 @@ -13555,10 +13131,6 @@ snapshots: atomic-sleep@1.0.0: {} - auto-text-size@0.2.3(react@19.2.1): - dependencies: - react: 19.2.1 - autoprefixer@10.4.21(postcss@8.5.6): dependencies: browserslist: 4.25.2 @@ -13643,14 +13215,14 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-styled-components@2.1.4(@babel/core@7.28.3)(styled-components@5.3.11(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react-is@18.3.1)(react@19.2.1))(supports-color@5.5.0): + babel-plugin-styled-components@2.1.4(@babel/core@7.28.3)(styled-components@5.3.11(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react-is@19.2.3)(react@19.2.1))(supports-color@5.5.0): dependencies: '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) lodash: 4.17.21 picomatch: 2.3.1 - styled-components: 5.3.11(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react-is@18.3.1)(react@19.2.1) + styled-components: 5.3.11(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react-is@19.2.3)(react@19.2.1) transitivePeerDependencies: - '@babel/core' - supports-color @@ -13764,8 +13336,6 @@ snapshots: brorand@1.1.0: {} - browserify-bignum@1.3.0-2: {} - browserslist@4.25.2: dependencies: caniuse-lite: 1.0.30001735 @@ -13802,8 +13372,6 @@ snapshots: buffer-crc32@0.2.13: {} - buffer-equal-constant-time@1.0.1: {} - buffer-from@1.1.2: {} buffer@6.0.3: @@ -13815,22 +13383,8 @@ snapshots: dependencies: node-gyp-build: 4.8.4 - bundle@2.1.0: {} - bytes-iec@3.1.1: {} - cacheable-lookup@5.0.4: {} - - cacheable-request@7.0.4: - dependencies: - clone-response: 1.0.3 - get-stream: 5.2.0 - http-cache-semantics: 4.2.0 - keyv: 4.5.4 - lowercase-keys: 2.0.0 - normalize-url: 6.1.0 - responselike: 2.0.1 - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -13868,16 +13422,6 @@ snapshots: canvas-confetti@1.9.3: {} - cbor-js@0.1.0: {} - - chakra-ui-steps@2.2.0(@chakra-ui/react@2.10.9(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1): - dependencies: - '@chakra-ui/react': 2.10.9(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) - '@emotion/react': 11.14.0(@types/react@18.3.23)(react@19.2.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@19.2.1))(@types/react@18.3.23)(react@19.2.1) - framer-motion: 11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) - react: 19.2.1 - chalk@3.0.0: dependencies: ansi-styles: 4.3.0 @@ -13960,10 +13504,6 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - clone-response@1.0.3: - dependencies: - mimic-response: 1.0.1 - clsx@1.2.1: {} clsx@2.1.1: {} @@ -13978,8 +13518,6 @@ snapshots: color-name@1.1.4: {} - color2k@2.0.3: {} - colorette@2.0.20: {} combined-stream@1.0.8: @@ -14021,10 +13559,6 @@ snapshots: cookie-es@1.2.2: {} - copy-to-clipboard@3.3.3: - dependencies: - toggle-selection: 1.0.6 - core-js-compat@3.45.1: dependencies: browserslist: 4.25.3 @@ -14041,10 +13575,6 @@ snapshots: crc-32@1.2.2: {} - crc@4.3.2(buffer@6.0.3): - optionalDependencies: - buffer: 6.0.3 - create-jest@29.7.0(@types/node@20.4.2)(babel-plugin-macros@3.1.0): dependencies: '@jest/types': 29.6.3 @@ -14082,10 +13612,6 @@ snapshots: dependencies: uncrypto: 0.1.3 - css-box-model@1.2.1: - dependencies: - tiny-invariant: 1.3.3 - css-color-keywords@1.0.0: {} css-to-react-native@3.2.0: @@ -14201,7 +13727,7 @@ snapshots: date-fns@2.30.0: dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 dateformat@4.6.3: {} @@ -14235,18 +13761,12 @@ snapshots: decode-uri-component@0.2.2: {} - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - dedent@1.6.0(babel-plugin-macros@3.1.0): optionalDependencies: babel-plugin-macros: 3.1.0 deepmerge@4.3.1: {} - defer-to-connect@2.0.1: {} - define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -14306,6 +13826,11 @@ snapshots: dom-accessibility-api@0.6.3: {} + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.28.4 + csstype: 3.1.3 + domexception@4.0.0: dependencies: webidl-conversions: 7.0.0 @@ -14329,10 +13854,6 @@ snapshots: eastasianwidth@0.2.0: {} - ecdsa-sig-formatter@1.0.11: - dependencies: - safe-buffer: 5.2.1 - eciesjs@0.4.15: dependencies: '@ecies/ciphers': 0.2.4(@noble/ciphers@1.3.0) @@ -14743,10 +14264,6 @@ snapshots: flow-enums-runtime@0.0.6: {} - focus-lock@1.3.6: - dependencies: - tslib: 2.8.1 - follow-redirects@1.15.11: {} for-each@0.3.5: @@ -14802,10 +14319,6 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) - framesync@6.1.2: - dependencies: - tslib: 2.4.0 - fresh@0.5.2: {} fs.realpath@1.0.0: {} @@ -14896,20 +14409,6 @@ snapshots: gopd@1.2.0: {} - got@11.8.6: - dependencies: - '@sindresorhus/is': 4.6.0 - '@szmarczak/http-timer': 4.0.6 - '@types/cacheable-request': 6.0.3 - '@types/responselike': 1.0.3 - cacheable-lookup: 5.0.4 - cacheable-request: 7.0.4 - decompress-response: 6.0.0 - http2-wrapper: 1.0.3 - lowercase-keys: 2.0.0 - p-cancelable: 2.1.1 - responselike: 2.0.1 - graceful-fs@4.2.11: {} graphql-request@6.1.0(encoding@0.1.13)(graphql@16.11.0): @@ -14994,17 +14493,12 @@ snapshots: dependencies: react-is: 16.13.1 - hpagent@0.1.2: - optional: true - html-encoding-sniffer@3.0.0: dependencies: whatwg-encoding: 2.0.0 html-escaper@2.0.2: {} - http-cache-semantics@4.2.0: {} - http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -15028,13 +14522,6 @@ snapshots: transitivePeerDependencies: - supports-color - http2-wrapper@1.0.3: - dependencies: - quick-lru: 5.1.1 - resolve-alpn: 1.2.1 - - http_ece@1.2.0: {} - https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -15613,8 +15100,6 @@ snapshots: js-sha3@0.8.0: {} - js-sha512@0.9.0: {} - js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -15665,8 +15150,6 @@ snapshots: jsesc@3.1.0: {} - json-buffer@3.0.1: {} - json-parse-even-better-errors@2.3.1: {} json-rpc-engine@6.1.0: @@ -15684,19 +15167,6 @@ snapshots: jsqr@1.4.0: {} - jssha@3.3.1: {} - - jwa@2.0.1: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - - jws@4.0.0: - dependencies: - jwa: 2.0.1 - safe-buffer: 5.2.1 - kapsule@1.16.3: dependencies: lodash-es: 4.17.21 @@ -15707,10 +15177,6 @@ snapshots: node-gyp-build: 4.8.4 readable-stream: 3.6.2 - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - keyvaluestorage-interface@1.0.0: {} kleur@3.0.3: {} @@ -15780,16 +15246,12 @@ snapshots: lodash-es@4.17.21: {} - lodash.clonedeep@4.5.0: {} - lodash.debounce@4.0.8: {} lodash.isequal@4.5.0: {} lodash.memoize@4.1.2: {} - lodash.mergewith@4.6.2: {} - lodash.sortby@4.7.0: {} lodash.throttle@4.1.1: {} @@ -15800,16 +15262,6 @@ snapshots: dependencies: js-tokens: 4.0.0 - lottie-react@2.4.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1): - dependencies: - lottie-web: 5.13.0 - react: 19.2.1 - react-dom: 19.2.1(react@19.2.1) - - lottie-web@5.13.0: {} - - lowercase-keys@2.0.0: {} - lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -16045,10 +15497,6 @@ snapshots: mimic-fn@2.1.0: {} - mimic-response@1.0.1: {} - - mimic-response@3.1.0: {} - min-indent@1.0.1: {} minimalistic-assert@1.0.1: {} @@ -16095,18 +15543,6 @@ snapshots: ms@2.1.3: {} - multicoin-address-validator@0.5.26: - dependencies: - base-x: 4.0.1 - browserify-bignum: 1.3.0-2 - buffer: 6.0.3 - bundle: 2.1.0 - cbor-js: 0.1.0 - crc: 4.3.2(buffer@6.0.3) - js-sha512: 0.9.0 - jssha: 3.3.1 - lodash.isequal: 4.5.0 - multiformats@9.9.0: {} mz@2.7.0: @@ -16157,19 +15593,6 @@ snapshots: - '@babel/core' - babel-plugin-macros - ngrok@4.3.3: - dependencies: - '@types/node': 8.10.66 - extract-zip: 2.0.1 - got: 11.8.6 - lodash.clonedeep: 4.5.0 - uuid: 8.3.2 - yaml: 1.10.2 - optionalDependencies: - hpagent: 0.1.2 - transitivePeerDependencies: - - supports-color - node-addon-api@2.0.2: {} node-fetch-native@1.6.7: {} @@ -16194,8 +15617,6 @@ snapshots: normalize-range@0.1.2: {} - normalize-url@6.1.0: {} - npm-run-path@4.0.1: dependencies: path-key: 3.1.1 @@ -16305,8 +15726,6 @@ snapshots: '@oxc-resolver/binding-win32-ia32-msvc': 11.6.1 '@oxc-resolver/binding-win32-x64-msvc': 11.6.1 - p-cancelable@2.1.1: {} - p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -16563,8 +15982,6 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - property-expr@2.0.6: {} - proxy-agent@6.5.0: dependencies: agent-base: 7.1.4 @@ -16647,8 +16064,6 @@ snapshots: quick-format-unescaped@4.0.4: {} - quick-lru@5.1.1: {} - radix3@1.1.2: {} randombytes@2.1.0: @@ -16657,13 +16072,6 @@ snapshots: range-parser@1.2.1: {} - react-clientside-effect@1.2.8(react@19.2.1): - dependencies: - '@babel/runtime': 7.28.3 - react: 19.2.1 - - react-csv@2.2.2: {} - react-devtools-core@6.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: shell-quote: 1.8.3 @@ -16677,25 +16085,11 @@ snapshots: react: 19.2.1 scheduler: 0.27.0 - react-fast-compare@3.2.2: {} - react-fast-marquee@1.6.5(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) - react-focus-lock@2.13.6(@types/react@18.3.23)(react@19.2.1): - dependencies: - '@babel/runtime': 7.28.3 - focus-lock: 1.3.6 - prop-types: 15.8.1 - react: 19.2.1 - react-clientside-effect: 1.2.8(react@19.2.1) - use-callback-ref: 1.3.3(@types/react@18.3.23)(react@19.2.1) - use-sidecar: 1.1.3(@types/react@18.3.23)(react@19.2.1) - optionalDependencies: - '@types/react': 18.3.23 - react-force-graph-2d@1.29.0(react@19.2.1): dependencies: force-graph: 1.51.0 @@ -16703,8 +16097,6 @@ snapshots: react: 19.2.1 react-kapsule: 2.5.7(react@19.2.1) - react-ga4@2.1.0: {} - react-hook-form@7.62.0(react@19.2.1): dependencies: react: 19.2.1 @@ -16715,21 +16107,23 @@ snapshots: react-is@18.3.1: {} + react-is@19.2.3: {} + react-kapsule@2.5.7(react@19.2.1): dependencies: jerrypick: 1.1.2 react: 19.2.1 - react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10): + react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10): dependencies: '@jest/create-cache-key-function': 29.7.0 '@react-native/assets-registry': 0.81.0 '@react-native/codegen': 0.81.0(@babel/core@7.28.3) - '@react-native/community-cli-plugin': 0.81.0(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@react-native/community-cli-plugin': 0.81.0(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@react-native/gradle-plugin': 0.81.0 '@react-native/js-polyfills': 0.81.0 '@react-native/normalize-colors': 0.81.0 - '@react-native/virtualized-lists': 0.81.0(@types/react@18.3.23)(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) + '@react-native/virtualized-lists': 0.81.0(@types/react@18.3.23)(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 @@ -16820,6 +16214,15 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) + react-transition-group@4.4.5(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + '@babel/runtime': 7.28.4 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + react-transition-state@1.1.5(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: react: 19.2.1 @@ -16915,8 +16318,6 @@ snapshots: resize-observer-polyfill@1.5.1: {} - resolve-alpn@1.2.1: {} - resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 @@ -16941,10 +16342,6 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - responselike@2.0.1: - dependencies: - lowercase-keys: 2.0.0 - reusify@1.1.0: {} rimraf@3.0.2: @@ -17339,19 +16736,19 @@ snapshots: strip-json-comments@5.0.3: {} - styled-components@5.3.11(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react-is@18.3.1)(react@19.2.1): + styled-components@5.3.11(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react-is@19.2.3)(react@19.2.1): dependencies: '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) '@babel/traverse': 7.28.3(supports-color@5.5.0) '@emotion/is-prop-valid': 1.3.1 '@emotion/stylis': 0.8.5 '@emotion/unitless': 0.7.5 - babel-plugin-styled-components: 2.1.4(@babel/core@7.28.3)(styled-components@5.3.11(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react-is@18.3.1)(react@19.2.1))(supports-color@5.5.0) + babel-plugin-styled-components: 2.1.4(@babel/core@7.28.3)(styled-components@5.3.11(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react-is@19.2.3)(react@19.2.1))(supports-color@5.5.0) css-to-react-native: 3.2.0 hoist-non-react-statics: 3.3.2 react: 19.2.1 react-dom: 19.2.1(react@19.2.1) - react-is: 18.3.1 + react-is: 19.2.3 shallowequal: 1.1.0 supports-color: 5.5.0 transitivePeerDependencies: @@ -17492,10 +16889,6 @@ snapshots: throat@5.0.0: {} - tiny-case@1.0.3: {} - - tiny-invariant@1.3.3: {} - tinycolor2@1.6.0: {} tinyglobby@0.2.14: @@ -17515,12 +16908,8 @@ snapshots: dependencies: is-number: 7.0.0 - toggle-selection@1.0.6: {} - toidentifier@1.0.1: {} - toposort@2.0.2: {} - totalist@3.0.1: {} tough-cookie@4.1.4: @@ -17566,8 +16955,6 @@ snapshots: tslib@1.14.1: {} - tslib@2.4.0: {} - tslib@2.8.1: {} tsx@4.20.4: @@ -17583,8 +16970,6 @@ snapshots: type-fest@0.7.1: {} - type-fest@2.19.0: {} - type-fest@4.41.0: {} typed-array-buffer@1.0.3: @@ -17716,8 +17101,6 @@ snapshots: utils-merge@1.0.1: {} - uuid@10.0.0: {} - uuid@8.3.2: {} uuid@9.0.1: {} @@ -17790,10 +17173,10 @@ snapshots: dependencies: xml-name-validator: 4.0.0 - wagmi@2.16.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.8.3)(@tanstack/react-query@5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76): + wagmi@2.16.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.8.3)(@tanstack/react-query@5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1))(@types/react@18.3.23)(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76): dependencies: - '@tanstack/react-query': 5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) - '@wagmi/connectors': 5.9.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(@wagmi/core@2.19.0(@tanstack/query-core@5.8.3)(@types/react@18.3.23)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + '@tanstack/react-query': 5.8.4(react-dom@19.2.1(react@19.2.1))(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10))(react@19.2.1) + '@wagmi/connectors': 5.9.3(@react-native-async-storage/async-storage@1.24.0(react-native@0.81.0(@babel/core@7.28.3)(@react-native/metro-config@0.81.0(@babel/core@7.28.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/react@18.3.23)(bufferutil@4.0.9)(react@19.2.1)(utf-8-validate@5.0.10)))(@types/react@18.3.23)(@wagmi/core@2.19.0(@tanstack/query-core@5.8.3)(@types/react@18.3.23)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.1))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(encoding@0.1.13)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) '@wagmi/core': 2.19.0(@tanstack/query-core@5.8.3)(@types/react@18.3.23)(immer@10.1.1)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) react: 19.2.1 use-sync-external-store: 1.4.0(react@19.2.1) @@ -17839,16 +17222,6 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 - web-push@3.6.7: - dependencies: - asn1.js: 5.4.1 - http_ece: 1.2.0 - https-proxy-agent: 7.0.6 - jws: 4.0.0 - minimist: 1.2.8 - transitivePeerDependencies: - - supports-color - webextension-polyfill@0.10.0: {} webidl-conversions@3.0.1: {} @@ -18067,13 +17440,6 @@ snapshots: yocto-queue@0.1.0: {} - yup@1.7.0: - dependencies: - property-expr: 2.0.6 - tiny-case: 1.0.3 - toposort: 2.0.2 - type-fest: 2.19.0 - zod-validation-error@3.5.3(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/scripts/generate.mjs b/scripts/generate.mjs deleted file mode 100644 index 8ed0c411c..000000000 --- a/scripts/generate.mjs +++ /dev/null @@ -1,7 +0,0 @@ -// Generates VAPID key pair for web push notifications and outputs them as environment variables -import webpush from 'web-push' - -const vapidKeys = webpush.generateVAPIDKeys() - -console.log('NEXT_PUBLIC_VAPID_PUBLIC_KEY=' + vapidKeys.publicKey) -console.log('VAPID_PRIVATE_KEY=' + vapidKeys.privateKey) diff --git a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx index ee4d3c824..1006dc888 100644 --- a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx +++ b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx @@ -1,13 +1,13 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import ErrorAlert from '@/components/Global/ErrorAlert' import NavHeader from '@/components/Global/NavHeader' -import TokenAmountInput from '@/components/Global/TokenAmountInput' -import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants' +import AmountInput from '@/components/Global/AmountInput' +import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' import { useOnrampFlow } from '@/context/OnrampFlowContext' import { useWallet } from '@/hooks/wallet/useWallet' -import { formatAmount } from '@/utils' +import { formatAmount } from '@/utils/general.utils' import { countryData } from '@/components/AddMoney/consts' import { type BridgeKycStatus } from '@/utils/bridge-accounts.utils' import { useWebSocket } from '@/hooks/useWebSocket' @@ -15,7 +15,6 @@ import { useAuth } from '@/context/authContext' import { useCreateOnramp } from '@/hooks/useCreateOnramp' import { useRouter, useParams } from 'next/navigation' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { useNonEurSepaRedirect } from '@/hooks/useNonEurSepaRedirect' import { formatUnits } from 'viem' import PeanutLoading from '@/components/Global/PeanutLoading' import EmptyState from '@/components/Global/EmptyStates/EmptyState' @@ -26,8 +25,6 @@ import { getCurrencyConfig, getCurrencySymbol, getMinimumAmount } from '@/utils/ import { OnrampConfirmationModal } from '@/components/AddMoney/components/OnrampConfirmationModal' import { InitiateBridgeKYCModal } from '@/components/Kyc/InitiateBridgeKYCModal' import InfoCard from '@/components/Global/InfoCard' -import { usePaymentStore } from '@/redux/hooks' -import { saveDevConnectIntent } from '@/utils' type AddStep = 'inputAmount' | 'kyc' | 'loading' | 'collectUserDetails' | 'showDetails' @@ -50,17 +47,9 @@ export default function OnrampBankPage() { const { balance } = useWallet() const { user, fetchUser } = useAuth() const { createOnramp, isLoading: isCreatingOnramp, error: onrampError } = useCreateOnramp() - const { parsedPaymentData } = usePaymentStore() const selectedCountryPath = params.country as string - // redirect to add-money if this is a non-eur sepa country (blocked) - useNonEurSepaRedirect({ - countryIdentifier: selectedCountryPath, - redirectPath: '/add-money', - shouldRedirect: true, - }) - const selectedCountry = useMemo(() => { if (!selectedCountryPath) return null return countryData.find((country) => country.type === 'country' && country.path === selectedCountryPath) @@ -126,8 +115,7 @@ export default function OnrampBankPage() { setError({ showError: false, errorMessage: '' }) return true } - const cleanedAmountStr = amountStr.replace(/,/g, '') - const amount = Number(cleanedAmountStr) + const amount = Number(amountStr) if (!Number.isFinite(amount)) { setError({ showError: true, errorMessage: 'Please enter a valid number.' }) return false @@ -173,26 +161,17 @@ export default function OnrampBankPage() { }) return } - const cleanedAmount = rawTokenAmount.replace(/,/g, '') - setAmountToOnramp(cleanedAmount) + setAmountToOnramp(rawTokenAmount) setShowWarningModal(false) setIsRiskAccepted(false) try { const onrampDataResponse = await createOnramp({ - amount: cleanedAmount, + amount: rawTokenAmount, country: selectedCountry, }) setOnrampData(onrampDataResponse) if (onrampDataResponse.transferId) { - // @dev: save devconnect intent if this is a devconnect flow - to be deleted post devconnect - saveDevConnectIntent( - user?.user?.userId, - parsedPaymentData, - cleanedAmount, - onrampDataResponse.transferId - ) - setStep('showDetails') } else { setError({ @@ -344,23 +323,22 @@ export default function OnrampBankPage() {
How much do you want to add?
- { - const formattedAmount = parseFloat(inputTokenAmount.replace(/,/g, '')) + const formattedAmount = parseFloat(inputTokenAmount) if (formattedAmount < 0.1) { setError('Minimum deposit using crypto is $0.1.') @@ -68,7 +68,7 @@ export default function AddMoneyCryptoDirectPage() { if (isPaymentSuccess) { return ( -
How much do you want to add?
- setInputTokenAmount(value || '')} + diff --git a/src/app/(mobile-ui)/add-money/crypto/page.tsx b/src/app/(mobile-ui)/add-money/crypto/page.tsx index 6fe433773..8d4481aad 100644 --- a/src/app/(mobile-ui)/add-money/crypto/page.tsx +++ b/src/app/(mobile-ui)/add-money/crypto/page.tsx @@ -1,164 +1,49 @@ 'use client' -import { ARBITRUM_ICON } from '@/assets' -import { CryptoSourceListCard } from '@/components/AddMoney/components/CryptoSourceListCard' -import { - CRYPTO_EXCHANGES, - CRYPTO_WALLETS, - type CryptoSource, - type CryptoToken, - DEPOSIT_CRYPTO_TOKENS, -} from '@/components/AddMoney/consts' -import { CryptoDepositQR } from '@/components/AddMoney/views/CryptoDepositQR.view' -import NetworkSelectionView, { type SelectedNetwork } from '@/components/AddMoney/views/NetworkSelection.view' -import TokenSelectionView from '@/components/AddMoney/views/TokenSelection.view' -import NavHeader from '@/components/Global/NavHeader' -import PeanutLoading from '@/components/Global/PeanutLoading' -import { PEANUT_WALLET_CHAIN } from '@/constants' +import RhinoDepositView from '@/components/AddMoney/views/RhinoDeposit.view' +import { useAuth } from '@/context/authContext' +import PaymentSuccessView from '@/features/payments/shared/components/PaymentSuccessView' import { useWallet } from '@/hooks/wallet/useWallet' +import { rhinoApi } from '@/services/rhino' +import type { RhinoChainType } from '@/services/services.types' +import { useQuery } from '@tanstack/react-query' import { useRouter } from 'next/navigation' -import { useEffect, useState } from 'react' -import TokenAndNetworkConfirmationModal from '@/components/Global/TokenAndNetworkConfirmationModal' +import { useCallback, useState } from 'react' -type AddMoneyCryptoStep = 'sourceSelection' | 'tokenSelection' | 'networkSelection' | 'riskModal' | 'qrScreen' - -interface AddMoneyCryptoPageProps { - headerTitle?: string - onBack?: () => void - depositAddress?: string -} - -const AddMoneyCryptoPage = ({ headerTitle, onBack, depositAddress }: AddMoneyCryptoPageProps) => { +const AddMoneyCryptoPage = () => { + const { user } = useAuth() const router = useRouter() - const { address: peanutWalletAddress, isConnected } = useWallet() - const [currentStep, setCurrentStep] = useState('qrScreen') - const [selectedSource, setSelectedSource] = useState(CRYPTO_EXCHANGES[3]) - const [selectedToken, setSelectedToken] = useState(DEPOSIT_CRYPTO_TOKENS[0]) - const [selectedNetwork, setSelectedNetwork] = useState({ - chainId: PEANUT_WALLET_CHAIN.id.toString(), - name: PEANUT_WALLET_CHAIN.name, - iconUrl: ARBITRUM_ICON, + const { address: peanutWalletAddress } = useWallet() + const [chainType, setChainType] = useState('EVM') + const [showSuccessView, setShowSuccessView] = useState(false) + const [depositedAmount, setDepositedAmount] = useState(0) + + const { data: depositAddressData, isLoading } = useQuery({ + queryKey: ['rhino-deposit-address', user?.user.userId, chainType], + queryFn: () => + rhinoApi.createDepositAddress(peanutWalletAddress as string, chainType, user?.user.userId as string), + enabled: !!user && !!peanutWalletAddress, + staleTime: 1000 * 60 * 60 * 24, // 24 hours }) - const [isRiskAccepted, setIsRiskAccepted] = useState(false) - - useEffect(() => { - if (isRiskAccepted) { - setCurrentStep('qrScreen') - } - }, [isRiskAccepted]) - const handleCryptoSourceSelected = (source: CryptoSource) => { - setSelectedSource(source) - setCurrentStep('tokenSelection') - } - - const handleTokenSelected = (token: CryptoToken) => { - setSelectedToken(token) - setCurrentStep('networkSelection') - } - - const handleNetworkSelected = (network: SelectedNetwork) => { - setSelectedNetwork(network) - setIsRiskAccepted(false) - setCurrentStep('riskModal') - } - - const resetSelections = () => { - setSelectedToken(null) - setSelectedNetwork(null) - setIsRiskAccepted(false) - } - - const handleBackToSourceSelection = () => { - setCurrentStep('sourceSelection') - setSelectedSource(null) - resetSelections() - } + const handleSuccess = useCallback((amount: number) => { + setDepositedAmount(amount) + setShowSuccessView(true) + }, []) - const handleBackToTokenSelection = () => { - setCurrentStep('tokenSelection') - setSelectedNetwork(null) - setIsRiskAccepted(false) - } - - const handleBackToNetworkSelectionFromRisk = () => { - setCurrentStep('networkSelection') - setIsRiskAccepted(false) - } - - if (currentStep === 'tokenSelection' && selectedSource) { - return ( - - ) - } - - if ((currentStep === 'networkSelection' || currentStep === 'riskModal') && selectedSource && selectedToken) { - return ( - <> - - {currentStep === 'riskModal' && selectedToken && selectedNetwork && ( - setIsRiskAccepted(true)} - /> - )} - - ) - } - - if (currentStep === 'qrScreen' && selectedSource && selectedToken && selectedNetwork) { - if (!isConnected) { - return - } - - // Ensure we have a valid deposit address - const finalDepositAddress = depositAddress ?? peanutWalletAddress - if (!finalDepositAddress) { - router.push('/') - return null - } - - return ( - router.back()} - /> - ) + if (showSuccessView) { + return } return ( -
- router.back()} /> -
-

Where are you adding from?

- - {/* Exchanges Section */} -
- -
- - {/* Wallets Section - with a top margin for separation */} -
- router.push('/add-money/crypto/direct')} - /> -
-
-
+ router.back()} + chainType={chainType} + depositAddressData={depositAddressData} + isDepositAddressDataLoading={isLoading} + setChainType={setChainType} + onSuccess={handleSuccess} + /> ) } diff --git a/src/app/(mobile-ui)/add-money/layout.tsx b/src/app/(mobile-ui)/add-money/layout.tsx index 3cd4d5856..117dcbc7d 100644 --- a/src/app/(mobile-ui)/add-money/layout.tsx +++ b/src/app/(mobile-ui)/add-money/layout.tsx @@ -1,6 +1,5 @@ -import { generateMetadata } from '@/app/metadata' import PageContainer from '@/components/0_Bruddle/PageContainer' -import React from 'react' +import { generateMetadata } from '@/app/metadata' export const metadata = generateMetadata({ title: 'Add Money | Peanut', diff --git a/src/app/(mobile-ui)/add-money/page.tsx b/src/app/(mobile-ui)/add-money/page.tsx index 2c826b9b9..d4cd1b2be 100644 --- a/src/app/(mobile-ui)/add-money/page.tsx +++ b/src/app/(mobile-ui)/add-money/page.tsx @@ -1,10 +1,10 @@ 'use client' import { AddWithdrawRouterView } from '@/components/AddWithdraw/AddWithdrawRouterView' -import { useOnrampFlow } from '@/context' +import { useOnrampFlow } from '@/context/OnrampFlowContext' import { useRouter } from 'next/navigation' import { useEffect } from 'react' -import { checkIfInternalNavigation } from '@/utils' +import { checkIfInternalNavigation, getRedirectUrl, clearRedirectUrl, getFromLocalStorage } from '@/utils/general.utils' export default function AddMoneyPage() { const router = useRouter() @@ -15,7 +15,21 @@ export default function AddMoneyPage() { }, []) const handleBack = () => { - // Check if the referrer is from the same domain (internal navigation) + // check if we have a saved redirect url (from request fulfillment or similar flows) + const redirectUrl = getRedirectUrl() + const fromRequestFulfillment = getFromLocalStorage('fromRequestFulfillment') + + if (redirectUrl && fromRequestFulfillment) { + // clear the flags and navigate to saved url + clearRedirectUrl() + if (typeof localStorage !== 'undefined') { + localStorage.removeItem('fromRequestFulfillment') + } + router.push(redirectUrl) + return + } + + // fallback to standard navigation logic const isInternalReferrer = checkIfInternalNavigation() if (isInternalReferrer && window.history.length > 1) { diff --git a/src/app/(mobile-ui)/claim/page.tsx b/src/app/(mobile-ui)/claim/page.tsx index e4603ca71..af6239118 100644 --- a/src/app/(mobile-ui)/claim/page.tsx +++ b/src/app/(mobile-ui)/claim/page.tsx @@ -1,6 +1,8 @@ import { Claim } from '@/components' -import { BASE_URL, PEANUT_WALLET_TOKEN_DECIMALS, PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants' -import { formatAmount, resolveAddressToUsername } from '@/utils' +import { BASE_URL } from '@/constants/general.consts' +import { PEANUT_WALLET_TOKEN_DECIMALS, PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants/zerodev.consts' +import { formatAmount } from '@/utils/general.utils' +import { resolveAddressToUsername } from '@/utils/ens.utils' import { type Metadata } from 'next' import getOrigin from '@/lib/hosting/get-origin' import { sendLinksApi } from '@/services/sendLinks' diff --git a/src/app/(mobile-ui)/dev/shake-test/page.tsx b/src/app/(mobile-ui)/dev/shake-test/page.tsx index 63ac9aaef..1dd86676c 100644 --- a/src/app/(mobile-ui)/dev/shake-test/page.tsx +++ b/src/app/(mobile-ui)/dev/shake-test/page.tsx @@ -6,7 +6,7 @@ import Card from '@/components/Global/Card' import NavHeader from '@/components/Global/NavHeader' import { shootDoubleStarConfetti } from '@/utils/confetti' import { getShakeClass, type ShakeIntensity } from '@/utils/perk.utils' -import { PERK_HOLD_DURATION_MS } from '@/constants' +import { PERK_HOLD_DURATION_MS } from '@/constants/general.consts' export default function DevShakeTestPage() { const [isShaking, setIsShaking] = useState(false) diff --git a/src/app/(mobile-ui)/history/page.tsx b/src/app/(mobile-ui)/history/page.tsx index 3645cf2b0..e7ab5083f 100644 --- a/src/app/(mobile-ui)/history/page.tsx +++ b/src/app/(mobile-ui)/history/page.tsx @@ -20,11 +20,11 @@ import { useQueryClient, type InfiniteData } from '@tanstack/react-query' import { useWebSocket } from '@/hooks/useWebSocket' import { useInfiniteScroll } from '@/hooks/useInfiniteScroll' import { TRANSACTIONS } from '@/constants/query.consts' -import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants' import type { HistoryResponse } from '@/hooks/useTransactionHistory' import { AccountType } from '@/interfaces' import { completeHistoryEntry } from '@/utils/history.utils' import { formatUnits } from 'viem' +import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' /** * displays the user's transaction history with infinite scrolling and date grouping. diff --git a/src/app/(mobile-ui)/home/page.tsx b/src/app/(mobile-ui)/home/page.tsx index a6a02c7ca..f3a542d54 100644 --- a/src/app/(mobile-ui)/home/page.tsx +++ b/src/app/(mobile-ui)/home/page.tsx @@ -1,18 +1,17 @@ 'use client' -import { Button, type ButtonSize, type ButtonVariant } from '@/components/0_Bruddle' +import { Button, type ButtonSize, type ButtonVariant } from '@/components/0_Bruddle/Button' import PageContainer from '@/components/0_Bruddle/PageContainer' import { Icon } from '@/components/Global/Icons/Icon' import Loading from '@/components/Global/Loading' import PeanutLoading from '@/components/Global/PeanutLoading' -//import RewardsModal from '@/components/Global/RewardsModal' import HomeHistory from '@/components/Home/HomeHistory' -//import RewardsCardModal from '@/components/Home/RewardsCardModal' import { UserHeader } from '@/components/UserHeader' import { useAuth } from '@/context/authContext' import { useWallet } from '@/hooks/wallet/useWallet' import { useUserStore } from '@/redux/hooks' -import { formatExtendedNumber, getUserPreferences, printableUsdc, updateUserPreferences, getRedirectUrl } from '@/utils' +import { formatExtendedNumber, getUserPreferences, updateUserPreferences, getRedirectUrl } from '@/utils/general.utils' +import { printableUsdc } from '@/utils/balance.utils' import { useDisconnect } from '@reown/appkit/react' import Link from 'next/link' import { useEffect, useMemo, useState, useCallback, lazy, Suspense } from 'react' @@ -21,7 +20,7 @@ import { useAccount } from 'wagmi' // import ReferralCampaignModal from '@/components/Home/ReferralCampaignModal' // import FloatingReferralButton from '@/components/Home/FloatingReferralButton' import { formatUnits } from 'viem' -import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants' +import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' import { PostSignupActionManager } from '@/components/Global/PostSignupActionManager' import { useWithdrawFlow } from '@/context/WithdrawFlowContext' import { useClaimBankFlow } from '@/context/ClaimBankFlowContext' @@ -38,7 +37,6 @@ import LazyLoadErrorBoundary from '@/components/Global/LazyLoadErrorBoundary' // Lazy load heavy modal components (~20-30KB each) to reduce initial bundle size // Components are only loaded when user triggers them // Wrapped in error boundaries to gracefully handle chunk load failures -const IOSInstallPWAModal = lazy(() => import('@/components/Global/IOSInstallPWAModal')) const BalanceWarningModal = lazy(() => import('@/components/Global/BalanceWarningModal')) const SetupNotificationsModal = lazy(() => import('@/components/Notifications/SetupNotificationsModal')) const NoMoreJailModal = lazy(() => import('@/components/Global/NoMoreJailModal')) @@ -68,7 +66,6 @@ export default function Home() { const { isUserKycApproved } = useKycStatus() const username = user?.user.username - const [showIOSPWAInstallModal, setShowIOSPWAInstallModal] = useState(false) const [showBalanceWarningModal, setShowBalanceWarningModal] = useState(false) // const [showReferralCampaignModal, setShowReferralCampaignModal] = useState(false) const [isPostSignupActionModalVisible, setIsPostSignupActionModalVisible] = useState(false) @@ -114,31 +111,6 @@ export default function Home() { } }, [isWagmiConnected, disconnectWagmi]) - // effect for showing iOS PWA Install modal - useEffect(() => { - if (typeof window !== 'undefined') { - const isIOS = deviceType === DeviceType.IOS - const isStandalone = window.matchMedia('(display-mode: standalone)').matches - const hasSeenModalThisSession = sessionStorage.getItem('hasSeenIOSPWAPromptThisSession') - const redirectUrl = getRedirectUrl() - - if ( - isIOS && - !isStandalone && - !hasSeenModalThisSession && - !user?.hasPwaInstalled && - !isPostSignupActionModalVisible && - !redirectUrl && - !showBalanceWarningModal - ) { - setShowIOSPWAInstallModal(true) - sessionStorage.setItem('hasSeenIOSPWAPromptThisSession', 'true') - } else { - setShowIOSPWAInstallModal(false) - } - } - }, [user?.hasPwaInstalled, isPostSignupActionModalVisible, deviceType, showBalanceWarningModal]) - // effect for showing balance warning modal // modal priority order: notifications -> kyc -> post signup -> ios pwa -> balance warning // this ensures users complete important actions before seeing the balance warning @@ -161,21 +133,12 @@ export default function Home() { !hasSeenBalanceWarning && !showPermissionModal && // highest priority !showKycModal && - !isPostSignupActionModalVisible && - !showIOSPWAInstallModal + !isPostSignupActionModalVisible ) { setShowBalanceWarningModal(true) } } - }, [ - balance, - isFetchingBalance, - showPermissionModal, - showKycModal, - isPostSignupActionModalVisible, - showIOSPWAInstallModal, - user, - ]) + }, [balance, isFetchingBalance, showPermissionModal, showKycModal, isPostSignupActionModalVisible, user]) if (isLoading) { return @@ -220,7 +183,7 @@ export default function Home() {
- +
{showPermissionModal && !showBalanceWarningModal && ( @@ -230,25 +193,7 @@ export default function Home() { )} - - {/* Render the new Rewards Modal - - */} - - {/* Render the new Rewards Card Modal - setIsRewardsModalOpen(false)} /> - */}
- {/* iOS PWA Install Modal */} - - - setShowIOSPWAInstallModal(false)} - /> - - - {/* Add Money Prompt Modal */} {/* TODO @dev Disabling this, re-enable after properly fixing */} {/* setShowAddMoneyPromptModal(false)} /> */} diff --git a/src/app/(mobile-ui)/layout.tsx b/src/app/(mobile-ui)/layout.tsx index e76e67b17..d3c4bd1bf 100644 --- a/src/app/(mobile-ui)/layout.tsx +++ b/src/app/(mobile-ui)/layout.tsx @@ -157,7 +157,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => { 'relative flex-1 overflow-y-auto bg-background p-6 pb-24 md:pb-6', !!isSupport && 'p-0 pb-20 md:p-6', !!isHome && 'p-0 md:p-6 md:pr-0', - isUserLoggedIn ? 'pb-24' : 'pb-6' + isUserLoggedIn ? 'pb-24' : 'pb-4' ) )} > diff --git a/src/app/(mobile-ui)/notifications/page.tsx b/src/app/(mobile-ui)/notifications/page.tsx index d82a15962..6a31eb355 100644 --- a/src/app/(mobile-ui)/notifications/page.tsx +++ b/src/app/(mobile-ui)/notifications/page.tsx @@ -11,7 +11,7 @@ import Image from 'next/image' import { PEANUTMAN_LOGO } from '@/assets' import Link from 'next/link' import EmptyState from '@/components/Global/EmptyStates/EmptyState' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' export default function NotificationsPage() { const loadingRef = useRef(null) diff --git a/src/app/(mobile-ui)/points/invites/page.tsx b/src/app/(mobile-ui)/points/invites/page.tsx index ad79c3c07..c1045c60c 100644 --- a/src/app/(mobile-ui)/points/invites/page.tsx +++ b/src/app/(mobile-ui)/points/invites/page.tsx @@ -13,7 +13,7 @@ import { useRouter } from 'next/navigation' import { STAR_STRAIGHT_ICON } from '@/assets' import Image from 'next/image' import EmptyState from '@/components/Global/EmptyStates/EmptyState' -import { getInitialsFromName } from '@/utils' +import { getInitialsFromName } from '@/utils/general.utils' import { type PointsInvite } from '@/services/services.types' const InvitesPage = () => { diff --git a/src/app/(mobile-ui)/points/page.tsx b/src/app/(mobile-ui)/points/page.tsx index fbf2fa6f4..55c04719b 100644 --- a/src/app/(mobile-ui)/points/page.tsx +++ b/src/app/(mobile-ui)/points/page.tsx @@ -12,15 +12,14 @@ import TransactionAvatarBadge from '@/components/TransactionDetails/TransactionA import { VerifiedUserLabel } from '@/components/UserHeader' import { useAuth } from '@/context/authContext' import { invitesApi } from '@/services/invites' -import { generateInviteCodeLink, generateInvitesShareText } from '@/utils' +import { generateInviteCodeLink, generateInvitesShareText, getInitialsFromName } from '@/utils/general.utils' import { useQuery } from '@tanstack/react-query' import { useRouter } from 'next/navigation' import { STAR_STRAIGHT_ICON, TIER_0_BADGE, TIER_1_BADGE, TIER_2_BADGE, TIER_3_BADGE } from '@/assets' import Image from 'next/image' import { pointsApi } from '@/services/points' import EmptyState from '@/components/Global/EmptyStates/EmptyState' -import { getInitialsFromName } from '@/utils' -import type { PointsInvite } from '@/services/services.types' +import { type PointsInvite } from '@/services/services.types' import { useEffect } from 'react' import InvitesGraph from '@/components/Global/InvitesGraph' diff --git a/src/app/(mobile-ui)/profile/backup/page.tsx b/src/app/(mobile-ui)/profile/backup/page.tsx new file mode 100644 index 000000000..eb0bb9794 --- /dev/null +++ b/src/app/(mobile-ui)/profile/backup/page.tsx @@ -0,0 +1,196 @@ +'use client' + +import PageContainer from '@/components/0_Bruddle/PageContainer' +import ActionModal from '@/components/Global/ActionModal' +import Card from '@/components/Global/Card' +import EmptyState from '@/components/Global/EmptyStates/EmptyState' +import InfoCard from '@/components/Global/InfoCard' +import NavHeader from '@/components/Global/NavHeader' +import NavigationArrow from '@/components/Global/NavigationArrow' +import { useDeviceType } from '@/hooks/useGetDeviceType' +import { useRouter } from 'next/navigation' +import { useState } from 'react' + +type FaqModal = 'lose-phone' | 'change-phone' | 'export-keys' | null + +export default function BackupPage() { + const router = useRouter() + const { deviceType } = useDeviceType() + const [activeModal, setActiveModal] = useState(null) + + const isAndroid = deviceType === 'android' + const accountType = isAndroid ? 'Google account' : 'Apple ID' + + const backupSteps = isAndroid + ? [ + { title: 'Open Settings', description: 'Google โ†’ All services' }, + { title: 'Find Password Manager', description: 'Autofill โ†’ Google Password Manager โ†’ Turn ON sync' }, + { title: 'Verify in Chrome', description: 'Settings โ†’ Passwords โ†’ Search "Peanut"' }, + ] + : [ + { title: 'Go to Settings', description: '[Your Name] โ†’ iCloud' }, + { title: 'Find Passwords & Keychain', description: 'Turn it on' }, + { title: 'Verify it worked', description: 'Settings โ†’ Passwords โ†’ Search "Peanut"' }, + ] + + const closeModal = () => setActiveModal(null) + + return ( + +
+ router.replace('/profile')} /> + + + +
+

Enable backup now

+ +
    + {backupSteps.map((step, index) => ( +
  1. +

    {step.title}

    +

    {step.description}

    +
  2. + ))} +
+
+ +
+ +
+

Common questions

+ setActiveModal('lose-phone')}> +
+

What if I lose my phone?

+ +
+
+ setActiveModal('change-phone')}> +
+

What if I change phone?

+ +
+
+ setActiveModal('export-keys')}> +
+

Why can't I export my private key?

+ +
+
+
+
+ + {/* FAQ Modal: What if I lose my phone? */} + + + +
+ } + /> + + {/* FAQ Modal: What if I change phone? */} + +
    +
  1. Verify backup is working (check step 3 above)
  2. +
  3. Know your {accountType} password
  4. +
  5. Keep old phone until new one works
  6. +
+ + + + + } + /> + + {/* FAQ Modal: Why can't I export my private key? */} + +
+

Passkeys are safer by design

+

+ Unlike traditional private keys that can be copied as text, passkeys are cryptographic + credentials that can't be: +

+
    +
  • Screenshot by accident
  • +
  • Sent in a text message
  • +
  • Saved in an unsafe note app
  • +
  • Stolen by malware
  • +
+
+
+

The tradeoff

+

+ You get better security, but passkeys are locked to your device ecosystem (Apple or + Google). That's why cloud backup is critical. +

+
+
+ + i + +

We're exploring advanced export options for power users in future updates.

+
+ + } + /> + + ) +} diff --git a/src/app/(mobile-ui)/profile/exchange-rate/page.tsx b/src/app/(mobile-ui)/profile/exchange-rate/page.tsx index 5d5b2479b..6f1b7a965 100644 --- a/src/app/(mobile-ui)/profile/exchange-rate/page.tsx +++ b/src/app/(mobile-ui)/profile/exchange-rate/page.tsx @@ -4,7 +4,7 @@ import PageContainer from '@/components/0_Bruddle/PageContainer' import ExchangeRateWidget from '@/components/Global/ExchangeRateWidget' import NavHeader from '@/components/Global/NavHeader' import { useWallet } from '@/hooks/wallet/useWallet' -import { printableUsdc } from '@/utils' +import { printableUsdc } from '@/utils/balance.utils' import { getExchangeRateWidgetRedirectRoute } from '@/utils/exchangeRateWidget.utils' import { useRouter } from 'next/navigation' import { useEffect } from 'react' diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index 4d8312ae9..47a7556d2 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -14,14 +14,21 @@ import NavHeader from '@/components/Global/NavHeader' import { MERCADO_PAGO, PIX, SIMPLEFI } from '@/assets/payment-apps' import Image from 'next/image' import PeanutLoading from '@/components/Global/PeanutLoading' -import TokenAmountInput from '@/components/Global/TokenAmountInput' +import AmountInput from '@/components/Global/AmountInput' import { useWallet } from '@/hooks/wallet/useWallet' import { useSignUserOp } from '@/hooks/wallet/useSignUserOp' -import { clearRedirectUrl, getRedirectUrl, isTxReverted, saveRedirectUrl, formatNumberForDisplay } from '@/utils' +import { + clearRedirectUrl, + getRedirectUrl, + isTxReverted, + saveRedirectUrl, + formatNumberForDisplay, +} from '@/utils/general.utils' import { getShakeClass, type ShakeIntensity } from '@/utils/perk.utils' import { calculateSavingsInCents, isArgentinaMantecaQrPayment, getSavingsMessage } from '@/utils/qr-payment.utils' import ErrorAlert from '@/components/Global/ErrorAlert' -import { PEANUT_WALLET_TOKEN_DECIMALS, TRANSACTIONS, PERK_HOLD_DURATION_MS } from '@/constants' +import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' +import { PERK_HOLD_DURATION_MS } from '@/constants/general.consts' import { MANTECA_DEPOSIT_ADDRESS } from '@/constants/manteca.consts' import { MIN_MANTECA_QR_PAYMENT_AMOUNT } from '@/constants/payment.consts' import { formatUnits, parseUnits } from 'viem' @@ -32,7 +39,6 @@ import { EHistoryEntryType, EHistoryUserRole } from '@/hooks/useTransactionHisto import { loadingStateContext } from '@/context' import { getCurrencyPrice } from '@/app/actions/currency' import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow' -import { usePendingTransactions } from '@/hooks/wallet/usePendingTransactions' import { captureException } from '@sentry/nextjs' import { isPaymentProcessorQR, @@ -56,9 +62,10 @@ import { usePointsCalculation } from '@/hooks/usePointsCalculation' import { useWebSocket } from '@/hooks/useWebSocket' import type { HistoryEntry } from '@/hooks/useTransactionHistory' import { completeHistoryEntry } from '@/utils/history.utils' -import { useSupportModalContext } from '@/context/SupportModalContext' +import { useModalsContext } from '@/context/ModalsContext' import maintenanceConfig from '@/config/underMaintenance.config' import PointsCard from '@/components/Common/PointsCard' +import { TRANSACTIONS } from '@/constants/query.consts' const MAX_QR_PAYMENT_AMOUNT = '2000' const MIN_QR_PAYMENT_AMOUNT = '0.1' @@ -112,7 +119,6 @@ export default function QRPayPage() { const { shouldBlockPay, kycGateState } = useQrKycGate(paymentProcessor) const queryClient = useQueryClient() - const { hasPendingTransactions } = usePendingTransactions() const [isShaking, setIsShaking] = useState(false) const [shakeIntensity, setShakeIntensity] = useState('none') const [isClaimingPerk, setIsClaimingPerk] = useState(false) @@ -126,7 +132,7 @@ export default function QRPayPage() { const [pendingSimpleFiPaymentId, setPendingSimpleFiPaymentId] = useState(null) const [isWaitingForWebSocket, setIsWaitingForWebSocket] = useState(false) const [shouldRetry, setShouldRetry] = useState(true) - const { setIsSupportModalOpen } = useSupportModalContext() + const { setIsSupportModalOpen } = useModalsContext() const [waitingForMerchantAmount, setWaitingForMerchantAmount] = useState(false) const retryCount = useRef(0) @@ -313,13 +319,10 @@ export default function QRPayPage() { // For dynamic QR codes with preset amounts: // paymentAssetAmount is in local currency (e.g., "92" BRL) // paymentAgainstAmount is the USD equivalent (e.g., "18.4" USD) - // TokenAmountInput expects tokenValue in USD, so we pass paymentAgainstAmount + // AmountInput expects tokenValue in USD, so we pass paymentAgainstAmount // It will convert to local currency for display using isInitialInputUsd=false setAmount(paymentLock.paymentAgainstAmount) setCurrencyAmount(paymentLock.paymentAssetAmount) - setWaitingForMerchantAmount(false) - setErrorInitiatingPayment(null) - setShouldRetry(false) } }, [paymentLock?.code, paymentProcessor]) @@ -474,11 +477,10 @@ export default function QRPayPage() { data: fetchedPaymentLock, isLoading: isLoadingPaymentLock, error: paymentLockError, - failureCount, failureReason: paymentLockFailureReason, } = useQuery({ queryKey: ['manteca-payment-lock', qrCode, timestamp], - queryFn: async ({ queryKey }) => { + queryFn: async () => { if (paymentProcessor !== 'MANTECA' || !qrCode || !isPaymentProcessorQR(qrCode)) { return null } @@ -493,15 +495,7 @@ export default function QRPayPage() { // Retry network/timeout errors up to 2 times (3 total attempts) return failureCount < 3 }, - retryDelay: (attemptIndex) => { - const delayMs = 3000 // 3s - const MAX_RETRIES = 2 - const attemptNumber = attemptIndex + 1 // attemptIndex is 0-based, display as 1-based - console.log( - `Payment lock fetch failed, retrying in ${delayMs}ms... (attempt ${attemptNumber}/${MAX_RETRIES})` - ) - return delayMs - }, + retryDelay: 3000, staleTime: 0, // Always fetch fresh data gcTime: 0, // Don't cache for garbage collection }) @@ -898,23 +892,11 @@ export default function QRPayPage() { // Check user balance and payment limits useEffect(() => { - // Skip balance check on success screen (balance may not have updated yet) - if (isSuccess) { - setBalanceErrorMessage(null) - return - } - - // Skip balance check if transaction is being processed - // isLoading covers the gap between sendMoney completing and completeQrPayment finishing - if (hasPendingTransactions || isWaitingForWebSocket || isLoading) { - return - } - if (!usdAmount || usdAmount === '0.00' || isNaN(Number(usdAmount)) || balance === undefined) { setBalanceErrorMessage(null) return } - const paymentAmount = parseUnits(usdAmount.replace(/,/g, ''), PEANUT_WALLET_TOKEN_DECIMALS) + const paymentAmount = parseUnits(usdAmount, PEANUT_WALLET_TOKEN_DECIMALS) // Manteca-specific validation (PIX, MercadoPago, QR3) if (paymentProcessor === 'MANTECA') { @@ -934,7 +916,7 @@ export default function QRPayPage() { } else { setBalanceErrorMessage(null) } - }, [usdAmount, balance, hasPendingTransactions, isWaitingForWebSocket, isSuccess, isLoading, paymentProcessor]) + }, [usdAmount, balance, paymentProcessor]) // Use points confetti hook for animation - must be called unconditionally usePointsConfetti(isSuccess && pointsData?.estimatedPoints ? pointsData.estimatedPoints : undefined, pointsDivRef) @@ -983,25 +965,11 @@ export default function QRPayPage() { }, [shouldRetry, handleSimplefiRetry]) useEffect(() => { - if (paymentProcessor === 'SIMPLEFI') { - if (waitingForMerchantAmount && !shouldRetry) { - if (retryCount.current < 3) { - retryCount.current++ - setTimeout(() => { - setShouldRetry(true) - }, 3000) - } else { - setWaitingForMerchantAmount(false) - setShowOrderNotReadyModal(true) - } - } - } else if (paymentProcessor === 'MANTECA') { - if (waitingForMerchantAmount && !isLoadingPaymentLock) { - setWaitingForMerchantAmount(false) - setShowOrderNotReadyModal(true) - } + if (waitingForMerchantAmount && !isLoadingPaymentLock) { + setWaitingForMerchantAmount(false) + setShowOrderNotReadyModal(true) } - }, [waitingForMerchantAmount, shouldRetry, isLoadingPaymentLock, paymentProcessor]) + }, [waitingForMerchantAmount, isLoadingPaymentLock]) const isLoadingKycState = kycGateState === QrKycState.LOADING @@ -1541,10 +1509,20 @@ export default function QRPayPage() { {/* Amount Card */} {currency && ( - )} {balanceErrorMessage && } @@ -1590,7 +1566,7 @@ export default function QRPayPage() { {isLoading || isWaitingForWebSocket ? isWaitingForWebSocket ? 'Processing Payment...' - : loadingState + : 'Loading...' : 'Pay'} @@ -1602,7 +1578,7 @@ export default function QRPayPage() { ) } -export const QrPayPageLoading = ({ message }: { message: string }) => { +const QrPayPageLoading = ({ message }: { message: string }) => { return (
diff --git a/src/app/(mobile-ui)/qr/[code]/page.tsx b/src/app/(mobile-ui)/qr/[code]/page.tsx index b76b9c2b8..ec6994235 100644 --- a/src/app/(mobile-ui)/qr/[code]/page.tsx +++ b/src/app/(mobile-ui)/qr/[code]/page.tsx @@ -1,16 +1,16 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import Card from '@/components/Global/Card' import NavHeader from '@/components/Global/NavHeader' -import { PEANUT_API_URL } from '@/constants' +import { PEANUT_API_URL } from '@/constants/general.consts' import { useAuth } from '@/context/authContext' import { useRouter, useParams } from 'next/navigation' import { useCallback, useEffect, useState } from 'react' import PeanutLoading from '@/components/Global/PeanutLoading' import ErrorAlert from '@/components/Global/ErrorAlert' import { Icon } from '@/components/Global/Icons/Icon' -import { saveRedirectUrl, generateInviteCodeLink, sanitizeRedirectURL } from '@/utils' +import { saveRedirectUrl, generateInviteCodeLink, sanitizeRedirectURL } from '@/utils/general.utils' import { getShakeClass } from '@/utils/perk.utils' import Cookies from 'js-cookie' import { useRedirectQrStatus } from '@/hooks/useRedirectQrStatus' diff --git a/src/app/(mobile-ui)/qr/[code]/success/page.tsx b/src/app/(mobile-ui)/qr/[code]/success/page.tsx index 1f2e50eed..f8627e897 100644 --- a/src/app/(mobile-ui)/qr/[code]/success/page.tsx +++ b/src/app/(mobile-ui)/qr/[code]/success/page.tsx @@ -1,9 +1,8 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import Card from '@/components/Global/Card' import NavHeader from '@/components/Global/NavHeader' -import { BASE_URL } from '@/constants' import { useRouter, useParams } from 'next/navigation' import { useEffect } from 'react' import PeanutLoading from '@/components/Global/PeanutLoading' @@ -12,6 +11,7 @@ import { confettiPresets } from '@/utils/confetti' import { useRedirectQrStatus } from '@/hooks/useRedirectQrStatus' import QRCodeWrapper from '@/components/Global/QRCodeWrapper' import { useToast } from '@/components/0_Bruddle/Toast' +import { BASE_URL } from '@/constants/general.consts' export default function RedirectQrSuccessPage() { const router = useRouter() diff --git a/src/app/(mobile-ui)/recover-funds/page.tsx b/src/app/(mobile-ui)/recover-funds/page.tsx index f3d6c5c93..2ce27cdd9 100644 --- a/src/app/(mobile-ui)/recover-funds/page.tsx +++ b/src/app/(mobile-ui)/recover-funds/page.tsx @@ -7,11 +7,12 @@ import { type IUserBalance } from '@/interfaces' import { useState, useEffect, useCallback, useContext } from 'react' import { useWallet } from '@/hooks/wallet/useWallet' import { fetchWalletBalances } from '@/app/actions/tokens' -import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN, nativeCurrencyAddresses } from '@/constants' -import { areEvmAddressesEqual, isTxReverted, getExplorerUrl, getChainName, getTokenLogo } from '@/utils' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants/zerodev.consts' +import { nativeCurrencyAddresses } from '@/constants/general.consts' +import { areEvmAddressesEqual, isTxReverted, getExplorerUrl, getChainName, getTokenLogo } from '@/utils/general.utils' import { type RecipientState } from '@/context/WithdrawFlowContext' import GeneralRecipientInput, { type GeneralRecipientUpdate } from '@/components/Global/GeneralRecipientInput' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import ErrorAlert from '@/components/Global/ErrorAlert' import Card from '@/components/Global/Card' import Image from 'next/image' @@ -21,10 +22,10 @@ import { erc20Abi, parseUnits, encodeFunctionData, formatUnits } from 'viem' import type { Address, Hash, TransactionReceipt } from 'viem' import { useRouter } from 'next/navigation' import { loadingStateContext } from '@/context' -import Icon from '@/components/Global/Icon' import { captureException } from '@sentry/nextjs' import { mainnet, base, linea } from 'viem/chains' import { getPublicClient } from '@/app/actions/clients' +import { Icon } from '@/components/Global/Icons/Icon' // Helper function to check if a token is native ETH const isNativeToken = (tokenAddress: string): boolean => { @@ -256,7 +257,7 @@ export default function RecoverFundsPage() { className="flex items-center gap-2 hover:underline" > View on explorer - +
diff --git a/src/app/(mobile-ui)/request/pay/page.tsx b/src/app/(mobile-ui)/request/pay/page.tsx index d2adc3f10..8f65bffda 100644 --- a/src/app/(mobile-ui)/request/pay/page.tsx +++ b/src/app/(mobile-ui)/request/pay/page.tsx @@ -1,6 +1,6 @@ import { PayRequestLink } from '@/components/Request/Pay/Pay' import { chargesApi } from '@/services/charges' -import { formatAmount, printableAddress } from '@/utils' +import { formatAmount, printableAddress } from '@/utils/general.utils' import { type Metadata } from 'next' export const dynamic = 'force-dynamic' diff --git a/src/app/(mobile-ui)/send/page.tsx b/src/app/(mobile-ui)/send/page.tsx index 751b72780..9ef6bab40 100644 --- a/src/app/(mobile-ui)/send/page.tsx +++ b/src/app/(mobile-ui)/send/page.tsx @@ -10,7 +10,7 @@ export const metadata = generateMetadata({ keywords: 'crypto transfer, send crypto, cross-chain transfer, offramp, digital dollars', }) -export default function SendPage() { +export default function DirectSendPage() { return ( diff --git a/src/app/(mobile-ui)/settings/page.tsx b/src/app/(mobile-ui)/settings/page.tsx index a04f8dfed..c0a4e80ee 100644 --- a/src/app/(mobile-ui)/settings/page.tsx +++ b/src/app/(mobile-ui)/settings/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { useAuth } from '@/context/authContext' import { useQueryClient } from '@tanstack/react-query' diff --git a/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx b/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx index dfefe5ac9..20f9b8506 100644 --- a/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx +++ b/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx @@ -1,13 +1,17 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { ALL_COUNTRIES_ALPHA3_TO_ALPHA2 } from '@/components/AddMoney/consts' import Card from '@/components/Global/Card' import ErrorAlert from '@/components/Global/ErrorAlert' import NavHeader from '@/components/Global/NavHeader' import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard' import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow' -import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN_SYMBOL, PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants' +import { + PEANUT_WALLET_CHAIN, + PEANUT_WALLET_TOKEN_SYMBOL, + PEANUT_WALLET_TOKEN_DECIMALS, +} from '@/constants/zerodev.consts' import { useWithdrawFlow } from '@/context/WithdrawFlowContext' import { useWallet } from '@/hooks/wallet/useWallet' import { usePendingTransactions } from '@/hooks/wallet/usePendingTransactions' @@ -15,8 +19,9 @@ import { AccountType, type Account } from '@/interfaces' import { formatIban, shortenStringLong, isTxReverted } from '@/utils/general.utils' import { useParams, useRouter } from 'next/navigation' import { useEffect, useState } from 'react' -import DirectSuccessView from '@/components/Payment/Views/Status.payment.view' -import { ErrorHandler, getBridgeChainName } from '@/utils' +import PaymentSuccessView from '@/features/payments/shared/components/PaymentSuccessView' +import { ErrorHandler } from '@/utils/sdkErrorHandler.utils' +import { getBridgeChainName } from '@/utils/bridge-accounts.utils' import { getOfframpCurrencyConfig } from '@/utils/bridge.utils' import { createOfframp, confirmOfframp } from '@/app/actions/offramp' import { useAuth } from '@/context/authContext' @@ -26,7 +31,6 @@ import { PointsAction } from '@/services/services.types' import { usePointsCalculation } from '@/hooks/usePointsCalculation' import { useSearchParams } from 'next/navigation' import { parseUnits } from 'viem' -import { useNonEurSepaRedirect } from '@/hooks/useNonEurSepaRedirect' type View = 'INITIAL' | 'SUCCESS' @@ -50,13 +54,6 @@ export default function WithdrawBankPage() { const [balanceErrorMessage, setBalanceErrorMessage] = useState(null) const { hasPendingTransactions } = usePendingTransactions() - // redirect to withdraw if this is a non-eur sepa country (blocked) - useNonEurSepaRedirect({ - countryIdentifier: country, - redirectPath: '/withdraw', - shouldRedirect: true, - }) - // check if we came from send flow - using method param to detect (only bank goes through this page) const methodParam = searchParams.get('method') const fromSendFlow = methodParam === 'bank' @@ -75,14 +72,6 @@ export default function WithdrawBankPage() { bankAccount?.id ) - // non-eur sepa countries that are currently experiencing issues - const isNonEuroSepaCountry = !!( - nonEuroCurrency && - nonEuroCurrency !== 'EUR' && - nonEuroCurrency !== 'USD' && - nonEuroCurrency !== 'MXN' - ) - useEffect(() => { if (!amountToWithdraw) { // If no amount, go back to main page @@ -289,7 +278,10 @@ export default function WithdrawBankPage() { /> - + {bankAccount?.type === AccountType.IBAN ? ( <> - {isNonEuroSepaCountry && ( -
-
-
โš ๏ธ
-
-

- Service Temporarily Unavailable -

-

- Withdrawals to {nonEuroCurrency} bank accounts are temporarily unavailable. - Please try again later. -

-
-
-
- )} - {error.showError ? ( )} {error.showError && } @@ -364,7 +339,7 @@ export default function WithdrawBankPage() { )} {view === 'SUCCESS' && ( - isSendingTx || isRecording, [isSendingTx, isRecording]) + + // helper to manage errors consistently const setError = useCallback( (error: string | null) => { setPaymentError(error) - dispatch(paymentActions.setError(error)) - // Also set the withdraw flow error state for display in InitialWithdrawView + // also set the withdraw flow error state for display in InitialWithdrawView setWithdrawError({ showError: !!error, errorMessage: error || '', }) }, - [setPaymentError, dispatch, setWithdrawError] + [setPaymentError, setWithdrawError] ) const clearErrors = useCallback(() => { setError(null) }, [setError]) + // reset on mount useEffect(() => { - dispatch(paymentActions.resetPaymentState()) - resetPaymentInitiator() - }, [dispatch, resetPaymentInitiator]) - + setChargeDetails(null) + setTransactionHash(null) + setPaymentDetails(null) + resetRouteCalculation() + resetPaymentRecorder() + }, [setChargeDetails, setTransactionHash, setPaymentDetails, resetRouteCalculation, resetPaymentRecorder]) + + // clear errors when amount changes useEffect(() => { if (amountToWithdraw) { clearErrors() - dispatch(paymentActions.setChargeDetails(null)) + setChargeDetails(null) } - }, [amountToWithdraw]) + }, [amountToWithdraw, clearErrors, setChargeDetails]) + // propagate route/record errors useEffect(() => { - setPaymentError(paymentErrorFromHook) - }, [paymentErrorFromHook]) + const error = routeError || recordError + if (error) { + setPaymentError(error) + } + }, [routeError, recordError, setPaymentError]) + // prepare transaction when entering confirm view useEffect(() => { - if (currentView === 'CONFIRM' && activeChargeDetailsFromStore && withdrawData) { + if (currentView === 'CONFIRM' && chargeDetails && withdrawData && address) { console.log('Preparing withdraw transaction details...') - console.dir(activeChargeDetailsFromStore) - prepareTransactionDetails({ - chargeDetails: activeChargeDetailsFromStore, - from: { + console.dir(chargeDetails) + calculateRoute({ + source: { address: address as Address, tokenAddress: PEANUT_WALLET_TOKEN, chainId: PEANUT_WALLET_CHAIN.id.toString(), }, + destination: { + recipientAddress: chargeDetails.requestLink.recipientAddress as Address, + tokenAddress: chargeDetails.tokenAddress as Address, + tokenAmount: chargeDetails.tokenAmount, + tokenDecimals: chargeDetails.tokenDecimals, + tokenType: Number(chargeDetails.tokenType), + chainId: chargeDetails.chainId, + }, usdAmount: usdAmount, + skipGasEstimate: true, // peanut wallet handles gas }) } - }, [currentView, activeChargeDetailsFromStore, withdrawData, prepareTransactionDetails, usdAmount, address]) + }, [currentView, chargeDetails, withdrawData, calculateRoute, usdAmount, address]) const handleSetupReview = useCallback( async (data: Omit) => { @@ -126,7 +155,7 @@ export default function WithdrawCryptoPage() { } clearErrors() - dispatch(paymentActions.setChargeDetails(null)) + setChargeDetails(null) setIsPreparingReview(true) try { @@ -176,9 +205,9 @@ export default function WithdrawCryptoPage() { throw new Error('Failed to create charge for withdrawal or charge ID missing.') } - const fullChargeDetails: TRequestChargeResponse = await chargesApi.get(createdCharge.data.id) + const fullChargeDetails = await chargesApi.get(createdCharge.data.id) - dispatch(paymentActions.setChargeDetails(fullChargeDetails)) + setChargeDetails(fullChargeDetails) setShowCompatibilityModal(true) } catch (err: any) { console.error('Error during setup review (request/charge creation):', err) @@ -188,103 +217,139 @@ export default function WithdrawCryptoPage() { setIsPreparingReview(false) } }, - [amountToWithdraw, dispatch, setCurrentView] + [ + amountToWithdraw, + clearErrors, + setChargeDetails, + setIsPreparingReview, + setWithdrawData, + setShowCompatibilityModal, + setError, + ] ) const handleCompatibilityProceed = useCallback(() => { setShowCompatibilityModal(false) - if (activeChargeDetailsFromStore && withdrawData) { + if (chargeDetails && withdrawData) { setCurrentView('CONFIRM') } else { console.error('Proceeding to confirm, but charge details or withdraw data are missing.') setError('Failed to load withdrawal details for confirmation. Please go back and try again.') } - }, [activeChargeDetailsFromStore, withdrawData, setCurrentView]) + }, [chargeDetails, withdrawData, setCurrentView, setShowCompatibilityModal, setError]) const handleConfirmWithdrawal = useCallback(async () => { - if (!activeChargeDetailsFromStore || !withdrawData || !amountToWithdraw) { + if (!chargeDetails || !withdrawData || !amountToWithdraw || !address) { console.error('Withdraw data, active charge details, or amount missing for final confirmation') setError('Essential withdrawal information is missing.') return } + if (!transactions || transactions.length === 0) { + console.error('No transactions prepared for withdrawal') + setError('Transaction not prepared. Please try again.') + return + } + clearErrors() - dispatch(paymentActions.setError(null)) + setIsSendingTx(true) - const paymentPayload: InitiatePaymentPayload = { - recipient: { - identifier: withdrawData.address, - recipientType: 'ADDRESS', - resolvedAddress: withdrawData.address, - }, - tokenAmount: amountToWithdraw, - chargeId: activeChargeDetailsFromStore.uuid, - skipChargeCreation: true, - } + try { + // send transactions via peanut wallet + const txResult = await sendTransactions(transactions, PEANUT_WALLET_CHAIN.id.toString()) + const receipt = txResult.receipt + const userOpHash = txResult.userOpHash + + // validate transaction + if (receipt !== null && isTxReverted(receipt)) { + throw new Error(`Transaction failed (reverted). Hash: ${receipt.transactionHash}`) + } - const result = await initiatePayment(paymentPayload) + const finalTxHash = receipt?.transactionHash ?? userOpHash + + // record payment to backend + const payment = await recordPayment({ + chargeId: chargeDetails.uuid, + chainId: PEANUT_WALLET_CHAIN.id.toString(), + txHash: finalTxHash, + tokenAddress: PEANUT_WALLET_TOKEN, + payerAddress: address as Address, + }) - if (result.success && result.txHash) { + setTransactionHash(finalTxHash) + setPaymentDetails(payment) triggerHaptic() setCurrentView('STATUS') - } else { - console.error('Withdrawal execution failed:', result.error) - const errMsg = result.error || 'Withdrawal processing failed.' + } catch (err) { + console.error('Withdrawal execution failed:', err) + const errMsg = ErrorHandler(err) setError(errMsg) + } finally { + setIsSendingTx(false) } }, [ - activeChargeDetailsFromStore, + chargeDetails, withdrawData, amountToWithdraw, - dispatch, - initiatePayment, + address, + transactions, + sendTransactions, + recordPayment, setCurrentView, - setAmountToWithdraw, - setWithdrawData, - setPaymentError, + setTransactionHash, + setPaymentDetails, + clearErrors, + setError, + triggerHaptic, ]) const handleRouteRefresh = useCallback(async () => { - if (!activeChargeDetailsFromStore) return + if (!chargeDetails || !address) return console.log('Refreshing withdraw route due to expiry...') - console.log('About to call prepareTransactionDetails with:', activeChargeDetailsFromStore) - await prepareTransactionDetails({ - chargeDetails: activeChargeDetailsFromStore, - from: { + await calculateRoute({ + source: { address: address as Address, tokenAddress: PEANUT_WALLET_TOKEN, chainId: PEANUT_WALLET_CHAIN.id.toString(), }, + destination: { + recipientAddress: chargeDetails.requestLink.recipientAddress as Address, + tokenAddress: chargeDetails.tokenAddress as Address, + tokenAmount: chargeDetails.tokenAmount, + tokenDecimals: chargeDetails.tokenDecimals, + tokenType: Number(chargeDetails.tokenType), + chainId: chargeDetails.chainId, + }, usdAmount: usdAmount, + skipGasEstimate: true, }) - }, [activeChargeDetailsFromStore, prepareTransactionDetails, usdAmount, address]) + }, [chargeDetails, calculateRoute, usdAmount, address]) const handleBackFromConfirm = useCallback(() => { setCurrentView('INITIAL') clearErrors() - dispatch(paymentActions.setError(null)) - dispatch(paymentActions.setChargeDetails(null)) - }, [dispatch, setCurrentView]) + setChargeDetails(null) + }, [setCurrentView, clearErrors, setChargeDetails]) - // Check if this is a cross-chain withdrawal (align with usePaymentInitiator logic) + // check if this is a cross-chain withdrawal const isCrossChainWithdrawal = useMemo(() => { - if (!withdrawData || !activeChargeDetailsFromStore) return false + if (!withdrawData || !chargeDetails) return false - // In withdraw flow, we're moving from Peanut Wallet to the selected chain - // This matches the logic in usePaymentInitiator for withdraw flows + // in withdraw flow, we're moving from Peanut Wallet to the selected chain const fromChainId = isPeanutWallet ? PEANUT_WALLET_CHAIN.id.toString() : withdrawData.chain.chainId - const toChainId = activeChargeDetailsFromStore.chainId + const toChainId = chargeDetails.chainId return fromChainId !== toChainId - }, [withdrawData, activeChargeDetailsFromStore, isPeanutWallet]) + }, [withdrawData, chargeDetails, isPeanutWallet]) // reset withdraw flow when this component unmounts useEffect(() => { return () => { - resetPaymentInitiator() + resetRouteCalculation() + resetPaymentRecorder() resetTokenContextProvider() // reset token selector context to make sure previously selected token is not cached } - }, [resetWithdrawFlow, resetPaymentInitiator]) + }, [resetRouteCalculation, resetPaymentRecorder, resetTokenContextProvider]) // Check for route type errors (similar to payment flow) const routeTypeError = useMemo(() => { @@ -333,7 +398,7 @@ export default function WithdrawCryptoPage() { /> )} - {currentView === 'CONFIRM' && withdrawData && activeChargeDetailsFromStore && ( + {currentView === 'CONFIRM' && withdrawData && chargeDetails && ( )} - {currentView === 'STATUS' && withdrawData && activeChargeDetailsFromStore && ( + {currentView === 'STATUS' && withdrawData && chargeDetails && ( <> - (undefined) const [currencyAmount, setCurrencyAmount] = useState(undefined) const [usdAmount, setUsdAmount] = useState(undefined) const [step, setStep] = useState('amountInput') const [balanceErrorMessage, setBalanceErrorMessage] = useState(null) const searchParams = useSearchParams() const paramAddress = searchParams.get('destination') + const isSavedAccount = searchParams.get('isSavedAccount') === 'true' const [destinationAddress, setDestinationAddress] = useState(paramAddress ?? '') const [selectedBank, setSelectedBank] = useState(null) const [accountType, setAccountType] = useState(null) @@ -71,11 +71,10 @@ export default function MantecaWithdrawFlow() { const { sendMoney, balance } = useWallet() const { isLoading, loadingState, setLoadingState } = useContext(loadingStateContext) const { user, fetchUser } = useAuth() - const { setIsSupportModalOpen } = useSupportModalContext() + const { setIsSupportModalOpen } = useModalsContext() const queryClient = useQueryClient() const { isUserBridgeKycApproved } = useKycStatus() const { hasPendingTransactions } = usePendingTransactions() - const swapCurrency = searchParams.get('swap-currency') // Get method and country from URL parameters const selectedMethodType = searchParams.get('method') // mercadopago, pix, bank-transfer, etc. const countryFromUrl = searchParams.get('country') // argentina, brazil, etc. @@ -88,10 +87,6 @@ export default function MantecaWithdrawFlow() { return countryData.find((country) => country.type === 'country' && country.path === countryPath) }, [countryPath]) - const isMantecaCountry = useMemo(() => { - return selectedCountry?.id && selectedCountry.id in MANTECA_COUNTRIES_CONFIG - }, [selectedCountry]) - const countryConfig = useMemo(() => { if (!selectedCountry) return undefined return MANTECA_COUNTRIES_CONFIG[selectedCountry.id] @@ -104,25 +99,6 @@ export default function MantecaWithdrawFlow() { isLoading: isCurrencyLoading, } = useCurrency(selectedCountry?.currency!) - // determine if initial input should be in usd or local currency - // for manteca countries, default to local currency unless explicitly overridden - const isInitialInputUsd = useMemo(() => { - // if swap-currency param is explicitly set to 'true' (user toggled to local currency) - // then show local currency first - if (swapCurrency === 'true') { - return false - } - - // if it's a manteca country, default to local currency (not usd) - // ignore swap-currency=false for manteca countries to ensure local currency default - if (isMantecaCountry) { - return false - } - - // otherwise default to usd (for non-manteca countries) - return true - }, [swapCurrency, isMantecaCountry]) - // Initialize KYC flow hook const { isMantecaKycRequired } = useMantecaKycFlow({ country: selectedCountry }) @@ -285,7 +261,6 @@ export default function MantecaWithdrawFlow() { const resetState = () => { setStep('amountInput') - setAmount(undefined) setCurrencyAmount(undefined) setUsdAmount(undefined) setDestinationAddress(paramAddress ?? '') @@ -314,7 +289,7 @@ export default function MantecaWithdrawFlow() { setBalanceErrorMessage(null) return } - const paymentAmount = parseUnits(usdAmount.replace(/,/g, ''), PEANUT_WALLET_TOKEN_DECIMALS) + const paymentAmount = parseUnits(usdAmount, PEANUT_WALLET_TOKEN_DECIMALS) if (paymentAmount < parseUnits(MIN_WITHDRAW_AMOUNT, PEANUT_WALLET_TOKEN_DECIMALS)) { setBalanceErrorMessage(`Withdraw amount must be at least $${MIN_WITHDRAW_AMOUNT}`) } else if (paymentAmount > parseUnits(MAX_WITHDRAW_AMOUNT, PEANUT_WALLET_TOKEN_DECIMALS)) { @@ -369,13 +344,7 @@ export default function MantecaWithdrawFlow() { {/* Points Display - ref used for confetti origin point */} {pointsData?.estimatedPoints && ( -
- star -

- You've earned {pointsData.estimatedPoints}{' '} - {pointsData.estimatedPoints === 1 ? 'point' : 'points'}! -

-
+ )}
@@ -442,27 +411,35 @@ export default function MantecaWithdrawFlow() { {step === 'amountInput' && (
Amount to withdraw
-
- diff --git a/src/app/(setup)/setup/page.tsx b/src/app/(setup)/setup/page.tsx index 9c64ff11c..2056c9f14 100644 --- a/src/app/(setup)/setup/page.tsx +++ b/src/app/(setup)/setup/page.tsx @@ -10,7 +10,7 @@ import { Suspense, useEffect, useState } from 'react' import { setupSteps as masterSetupSteps } from '../../../components/Setup/Setup.consts' import UnsupportedBrowserModal from '@/components/Global/UnsupportedBrowserModal' import { isLikelyWebview, isDeviceOsSupported } from '@/components/Setup/Setup.utils' -import { getFromCookie } from '@/utils' +import { getFromCookie } from '@/utils/general.utils' import { DeviceType, useDeviceType } from '@/hooks/useGetDeviceType' import { useAuth } from '@/context/authContext' diff --git a/src/app/ClientProviders.tsx b/src/app/ClientProviders.tsx new file mode 100644 index 000000000..0f2e846fb --- /dev/null +++ b/src/app/ClientProviders.tsx @@ -0,0 +1,30 @@ +'use client' + +/** + * wrapper for client-side providers + * + * groups all client providers in one place, keeping the root layout clean. + * the root layout (server component) renders this single client boundary. + */ +import { ConsoleGreeting } from '@/components/Global/ConsoleGreeting' +import { ScreenOrientationLocker } from '@/components/Global/ScreenOrientationLocker' +import { TranslationSafeWrapper } from '@/components/Global/TranslationSafeWrapper' +import { PeanutProvider } from '@/config' +import { ContextProvider } from '@/context' +import { FooterVisibilityProvider } from '@/context/footerVisibility' + +export function ClientProviders({ children }: { children: React.ReactNode }) { + return ( + + + + + + + {children} + + + + + ) +} diff --git a/src/app/[...recipient]/client.tsx b/src/app/[...recipient]/client.tsx index bc711c0d0..0c158292a 100644 --- a/src/app/[...recipient]/client.tsx +++ b/src/app/[...recipient]/client.tsx @@ -1,678 +1,73 @@ 'use client' -import { type StatusType } from '@/components/Global/Badges/StatusBadge' -import PeanutLoading from '@/components/Global/PeanutLoading' -import ConfirmPaymentView from '@/components/Payment/Views/Confirm.payment.view' -import ValidationErrorView, { type ValidationErrorViewProps } from '@/components/Payment/Views/Error.validation.view' -import InitialPaymentView from '@/components/Payment/Views/Initial.payment.view' -import DirectSuccessView from '@/components/Payment/Views/Status.payment.view' +import { useSearchParams, useRouter } from 'next/navigation' +import { ContributePotPageWrapper } from '@/features/payments/flows/contribute-pot/ContributePotPageWrapper' +import { SemanticRequestPageWrapper } from '@/features/payments/flows/semantic-request/SemanticRequestPageWrapper' +import { isAddress } from 'viem' import PublicProfile from '@/components/Profile/components/PublicProfile' -import { TransactionDetailsReceipt } from '@/components/TransactionDetails/TransactionDetailsReceipt' -import { type TransactionDetails } from '@/components/TransactionDetails/transactionTransformer' import { useAuth } from '@/context/authContext' -import { useCurrency } from '@/hooks/useCurrency' -import { useTransactionDetailsDrawer } from '@/hooks/useTransactionDetailsDrawer' -import { EHistoryEntryType, EHistoryUserRole } from '@/hooks/useTransactionHistory' -import { useUserInteractions } from '@/hooks/useUserInteractions' -import { EParseUrlError, parsePaymentURL, type ParseUrlError } from '@/lib/url-parser/parser' -import { type ParsedURL } from '@/lib/url-parser/types/payment' -import { useAppDispatch, usePaymentStore } from '@/redux/hooks' -import { paymentActions } from '@/redux/slices/payment-slice' -import { chargesApi } from '@/services/charges' -import { requestsApi } from '@/services/requests' -import { formatAmount, getInitialsFromName, updateUserPreferences, getUserPreferences } from '@/utils' -import { useRouter, useSearchParams } from 'next/navigation' -import { useEffect, useMemo, useRef, useState } from 'react' -import { twMerge } from 'tailwind-merge' -import { fetchTokenPrice } from '@/app/actions/tokens' -import { RequestFulfillmentBankFlowStep, useRequestFulfillmentFlow } from '@/context/RequestFulfillmentFlowContext' -import ActionList from '@/components/Common/ActionList' -import NavHeader from '@/components/Global/NavHeader' -import { ReqFulfillBankFlowManager } from '@/components/Request/views/ReqFulfillBankFlowManager' -import SupportCTA from '@/components/Global/SupportCTA' -import { BankRequestType, useDetermineBankRequestType } from '@/hooks/useDetermineBankRequestType' -import { PointsAction } from '@/services/services.types' -import { usePointsCalculation } from '@/hooks/usePointsCalculation' -import { useHaptic } from 'use-haptic' -import { MAX_DEVCONNECT_INTENTS } from '@/constants' +import { ValidatedUsernameWrapper } from '@/components/Username/ValidatedUsernameWrapper' +// kept for backward compatibility with old payment form export type PaymentFlow = 'request_pay' | 'external_wallet' | 'direct_pay' | 'withdraw' + interface Props { recipient: string[] - flow?: PaymentFlow } -export default function PaymentPage({ recipient, flow = 'request_pay' }: Props) { - const isDirectPay = flow === 'direct_pay' - const isExternalWalletFlow = flow === 'external_wallet' - const dispatch = useAppDispatch() - const { currentView, parsedPaymentData, chargeDetails, paymentDetails, usdAmount, isDaimoPaymentProcessing } = - usePaymentStore() - const [error, setError] = useState(null) - const [isUrlParsed, setIsUrlParsed] = useState(false) - const [isRequestDetailsFetching, setIsRequestDetailsFetching] = useState(false) - const { user, isFetchingUser } = useAuth() +export default function PaymentPage({ recipient }: Props) { const searchParams = useSearchParams() - const chargeId = searchParams.get('chargeId') - const requestId = searchParams.get('id') const router = useRouter() - const { - code: currencyCode, - symbol: currencySymbol, - price: currencyPrice, - } = useCurrency(searchParams.get('currency')) - const [currencyAmount, setCurrencyAmount] = useState('') - const { isDrawerOpen, selectedTransaction, openTransactionDetails } = useTransactionDetailsDrawer() - const [isLinkCancelling, setisLinkCancelling] = useState(false) - const { - showRequestFulfilmentBankFlowManager, - setShowRequestFulfilmentBankFlowManager, - setFlowStep: setRequestFulfilmentBankFlowStep, - } = useRequestFulfillmentFlow() - const { requestType } = useDetermineBankRequestType(chargeDetails?.requestLink.recipientAccount.userId ?? '') - const { triggerHaptic } = useHaptic() - - // Calculate points API call - // Points are ALWAYS calculated based on USD value (per PR.md: "1c in cost = 10 pts") - // Pre-fetch points when in confirm view (request_pay) or status view (direct_pay) - // For request_pay: calculate on CONFIRM (before payment) - // For direct_pay: calculate on STATUS (after payment completes) - - const shouldFetchPoints = - usdAmount && - chargeDetails?.uuid && - ((flow === 'request_pay' && currentView === 'CONFIRM') || (flow === 'direct_pay' && currentView === 'STATUS')) - - const { pointsData } = usePointsCalculation( - PointsAction.P2P_REQUEST_PAYMENT, - usdAmount, - !!shouldFetchPoints, - chargeDetails?.uuid, - chargeDetails?.requestLink.recipientAccount.userId - ) - - // determine if the current user is the recipient of the transaction - const isCurrentUserRecipient = chargeDetails?.requestLink.recipientAccount?.userId === user?.user.userId - - // determine the counterparty of the transaction - const payer = chargeDetails?.payments && chargeDetails?.payments.length > 0 ? chargeDetails.payments[0] : null - const counterpartyUserId = isCurrentUserRecipient - ? payer?.payerAccount?.userId - : chargeDetails?.requestLink.recipientAccount?.userId - - // fetch interactions for the counterparty - const { interactions } = useUserInteractions(counterpartyUserId ? [counterpartyUserId] : []) - - const isMountedRef = useRef(true) - - const fetchChargeDetails = async () => { - if (!chargeId) return - chargesApi - .get(chargeId) - .then(async (charge) => { - dispatch(paymentActions.setChargeDetails(charge)) - - const isCurrencyValueReliable = - charge.currencyCode === 'USD' && - charge.currencyAmount && - String(charge.currencyAmount) !== String(charge.tokenAmount) - - if (isCurrencyValueReliable) { - dispatch(paymentActions.setUsdAmount(Number(charge.currencyAmount).toFixed(2))) - } else { - const priceData = await fetchTokenPrice(charge.tokenAddress, charge.chainId) - if (priceData?.price) { - const usdValue = Number(charge.tokenAmount) * priceData.price - dispatch(paymentActions.setUsdAmount(usdValue.toFixed(2))) - } - } - - // check latest payment status if payments exist - if (charge.payments && charge.payments.length > 0) { - const latestPayment = charge.payments[charge.payments.length - 1] + const { user } = useAuth() + const requestId = searchParams.get('id') + const chargeIdFromUrl = searchParams.get('chargeId') - // show STATUS view for any payment attempt (including failed ones) - if (latestPayment.status !== 'NEW') { - triggerHaptic() - dispatch(paymentActions.setView('STATUS')) - } - } - }) - .catch((_err) => { - setError(getDefaultError(!!user)) - }) + // request pot flow: ?id= + if (requestId) { + return } - // prevent memory leaks - useEffect(() => { - isMountedRef.current = true - return () => { - isMountedRef.current = false - } - }, []) - - useEffect(() => { - if (!parsedPaymentData) { - setIsUrlParsed(false) - setError(null) - } - }, [parsedPaymentData]) - - useEffect(() => { - let isMounted = true - const fetchParsedURL = async () => { - const { parsedUrl, error } = await parsePaymentURL(recipient) - - if (!isMounted) return - - if (parsedUrl) { - const amount = parsedUrl.amount ? formatAmount(parsedUrl.amount || '') : undefined - const updatedParsedData = { - ...parsedUrl, - amount, - } - dispatch(paymentActions.setParsedPaymentData(updatedParsedData)) - setIsUrlParsed(true) - - // ------------------------------------------------------------------------------------------------ - // @dev: save devconnect flow info to user preferences so it persists across navigation - // this is needed because payment state gets reset on unmount - if (updatedParsedData.isDevConnectFlow && user?.user?.userId) { - // validate required fields before storing (amount is optional at this stage) - const recipientAddress = updatedParsedData.recipient?.resolvedAddress - const chainId = updatedParsedData.chain?.chainId - const amount = updatedParsedData.amount - - if (recipientAddress && chainId) { - // create deterministic id based on recipient + chain only (not amount, as amount changes during flow) - const createDeterministicId = (addr: string, chain: string): string => { - const str = `${addr.toLowerCase()}-${chain.toLowerCase()}` - let hash = 0 - for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i) - hash = (hash << 5) - hash + char - hash = hash & hash // convert to 32bit integer - } - return Math.abs(hash).toString(36) - } - - const intentId = createDeterministicId(recipientAddress, chainId) - const prefs = getUserPreferences(user.user.userId) - const existingIntents = prefs?.devConnectIntents ?? [] - - // check if intent with same id already exists - const existingIntent = existingIntents.find((intent) => intent.id === intentId) - - if (!existingIntent) { - // create new intent - const sortedIntents = existingIntents.sort((a, b) => b.createdAt - a.createdAt) - const prunedIntents = sortedIntents.slice(0, MAX_DEVCONNECT_INTENTS - 1) - - updateUserPreferences(user.user.userId, { - devConnectIntents: [ - { - id: intentId, - recipientAddress, - chain: chainId, - amount: amount || '', - createdAt: Date.now(), - status: 'pending', - }, - ...prunedIntents, - ], - }) - } else if (amount && amount !== existingIntent.amount) { - // update existing intent with new amount if provided - const updatedIntents = existingIntents.map((intent) => - intent.id === intentId ? { ...intent, amount, createdAt: Date.now() } : intent - ) - updateUserPreferences(user.user.userId, { - devConnectIntents: updatedIntents, - }) - } - } - - // ------------------------------------------------------------------------------------------------ - } - - // render PUBLIC_PROFILE view if applicable - if ( - updatedParsedData.recipient?.recipientType === 'USERNAME' && - !updatedParsedData.amount && - !chargeId && - !requestId && - !isDirectPay && - !isExternalWalletFlow - ) { - dispatch(paymentActions.setView('PUBLIC_PROFILE')) - } else { - dispatch(paymentActions.setView('INITIAL')) - } - } else { - setError(getErrorProps({ error, isUser: !!user, recipient })) - } - } - - if (!isUrlParsed) { - fetchParsedURL() - } - - return () => { - isMounted = false - } - }, [recipient, user, isUrlParsed, dispatch, isDirectPay, chargeId, requestId]) - - // handle validation and charge creation - useEffect(() => { - // always show initial view, to let payer select token/chain of choice - if (chargeId) { - fetchChargeDetails() - } - }, [chargeId, dispatch, user]) - - // fetch requests for the recipient only when id is not available in the URL - useEffect(() => { - async function fetchRequests() { - if (!parsedPaymentData?.recipient) return - - try { - let recipientIdentifier: string | null = parsedPaymentData.recipient.identifier - - if (!recipientIdentifier) { - throw new Error('Not a valid recipient') - } - - const tokenAddress = - parsedPaymentData.token && parsedPaymentData.chain && parsedPaymentData.token.address - - const chainId = parsedPaymentData?.chain?.chainId ? parsedPaymentData?.chain?.chainId : undefined - - // conditional request params - const requestParams: any = { recipient: recipientIdentifier } - - // only include amount in search params if explicitly provided in URL - if (parsedPaymentData.amount && parsedPaymentData.amount !== '') { - requestParams.tokenAmount = parsedPaymentData.amount - } - - if (chainId) requestParams.chainId = chainId - if (tokenAddress) requestParams.tokenAddress = tokenAddress - - // fetch requests using the resolved address - const fetchedRequest = await requestsApi.search(requestParams) - - // if we have a request and the URL didn't specify an amount, - // update the parsedPaymentData to include the amount from the request - if (fetchedRequest && (!parsedPaymentData.amount || parsedPaymentData.amount === '')) { - dispatch( - paymentActions.setParsedPaymentData({ - ...parsedPaymentData, - amount: fetchedRequest.tokenAmount ? formatAmount(fetchedRequest.tokenAmount) : undefined, - }) - ) - } - - dispatch(paymentActions.setRequestDetails(fetchedRequest)) - } catch (_error) { - setError(getDefaultError(!!user)) - } - } - if (!requestId) { - fetchRequests() - } - }, [parsedPaymentData?.recipient, parsedPaymentData?.chain, parsedPaymentData?.token, parsedPaymentData?.amount]) - - // fetch request details if request ID is available - useEffect(() => { - if (requestId) { - setIsRequestDetailsFetching(true) - requestsApi - .get(requestId) - .then((request) => { - if (!isMountedRef.current) return - dispatch(paymentActions.setRequestDetails(request)) - dispatch(paymentActions.setView('INITIAL')) - }) - .catch((_err) => { - if (!isMountedRef.current) return - setError(getDefaultError(!!user)) - }) - .finally(() => { - if (isMountedRef.current) { - setIsRequestDetailsFetching(false) - } - }) - } else { - setIsRequestDetailsFetching(false) - } - }, [requestId, dispatch, user]) - - // reset payment state when navigating to a new payment page - useEffect(() => { - if (!chargeId) { - dispatch(paymentActions.resetPaymentState()) - setIsUrlParsed(false) - } - }, [dispatch, chargeId]) - - const transactionForDrawer: TransactionDetails | null = useMemo(() => { - if (!chargeDetails) return null - - let status: StatusType - switch (chargeDetails.timeline[0].status) { - case 'NEW': - case 'PENDING': - status = 'pending' - break - case 'SIGNED': - status = 'processing' - break - case 'COMPLETED': - case 'SUCCESSFUL': - status = 'completed' - break - case 'CANCELLED': - status = 'cancelled' - break - case 'FAILED': - case 'EXPIRED': - status = 'failed' - break - default: - status = 'pending' - break - } - - const recipientAccount = chargeDetails.requestLink.recipientAccount - const isCurrentUser = recipientAccount?.userId === user?.user.userId - - if (status === 'pending' && !isCurrentUser) { - return null - } - - const payerAccount = - chargeDetails.payments && chargeDetails.payments.length > 0 ? chargeDetails.payments[0].payerAccount : null - - // determine who the counterparty is. - // if the current user is the recipient of the funds, the counterparty is the one who paid. - // otherwise, the counterparty is the one who will receive the funds. - const counterparty = isCurrentUser ? payerAccount : recipientAccount - - const username = - counterparty?.user?.username || - counterparty?.identifier || - (isCurrentUser && chargeDetails.payments.length > 0 - ? chargeDetails.payments[0].payerAddress - : chargeDetails.requestLink.recipientAddress) - - const originalUserRole = isCurrentUser ? EHistoryUserRole.RECIPIENT : EHistoryUserRole.SENDER - let details: Partial = { - id: chargeDetails.uuid, - status, - amount: Number(chargeDetails.tokenAmount), - createdAt: new Date(paymentDetails?.createdAt ?? chargeDetails.createdAt), - tokenSymbol: chargeDetails.tokenSymbol, - initials: getInitialsFromName(username ?? ''), - memo: chargeDetails.requestLink.reference ?? undefined, - attachmentUrl: chargeDetails.requestLink.attachmentUrl ?? undefined, - completedAt: status === 'completed' ? new Date(chargeDetails.timeline[0].time) : undefined, - cancelledDate: status === 'cancelled' ? new Date(chargeDetails.timeline[0].time) : undefined, - extraDataForDrawer: { - isLinkTransaction: originalUserRole === EHistoryUserRole.SENDER && isCurrentUser, - originalType: EHistoryEntryType.REQUEST, - originalUserRole: originalUserRole, - link: window.location.href, - }, - userName: username ?? chargeDetails.requestLink.recipientAddress, - sourceView: 'history', - peanutFeeDetails: { - amountDisplay: '$ 0.00', - }, - currency: usdAmount ? { amount: usdAmount, code: 'USD' } : undefined, - isVerified: counterparty?.user?.bridgeKycStatus === 'approved', - haveSentMoneyToUser: counterparty?.userId ? interactions[counterparty.userId] || false : false, - } - - if (isExternalWalletFlow) { - details.extraDataForDrawer = { - isLinkTransaction: false, - originalType: EHistoryEntryType.DEPOSIT, - originalUserRole: EHistoryUserRole.SENDER, - } - details.direction = 'add' - details.userName = user?.user.username ?? undefined - details.initials = getInitialsFromName(user?.user.fullName ?? user?.user?.username ?? 'PU') - } - - return details as TransactionDetails - }, [ - chargeDetails, - user?.user.userId, - isExternalWalletFlow, - user?.user.username, - usdAmount, - paymentDetails, - interactions, - ]) - - useEffect(() => { - if (!transactionForDrawer) return - - // if add money flow and in initial or confirm view, don't auto set status - if (isExternalWalletFlow && (currentView === 'INITIAL' || currentView === 'CONFIRM') && !chargeId) { - return - } - - // show status view only if fulfillment payment is successful - if (chargeDetails?.fulfillmentPayment?.status === 'SUCCESSFUL') { - triggerHaptic() - dispatch(paymentActions.setView('STATUS')) - } - - // only open transaction details drawer if not add money flow - if (!isExternalWalletFlow) { - openTransactionDetails(transactionForDrawer) - } - }, [transactionForDrawer, currentView, dispatch, openTransactionDetails, isExternalWalletFlow, chargeId]) - - const showActionList = flow !== 'direct_pay' || (flow === 'direct_pay' && !user) // Show for direct-pay when user is not logged in - // Send to bank step if its mentioned in the URL and guest KYC is not needed - useEffect(() => { - const stepFromURL = searchParams.get('step') - if ( - parsedPaymentData && - chargeDetails && - requestType !== BankRequestType.GuestKycNeeded && - stepFromURL === 'bank' - ) { - setShowRequestFulfilmentBankFlowManager(true) + // need at least one recipient segment + if (recipient.length === 0) { + return null + } - setRequestFulfilmentBankFlowStep(RequestFulfillmentBankFlowStep.BankCountryList) - } - }, [searchParams, parsedPaymentData, chargeDetails, requestType]) + const firstSegment = decodeURIComponent(recipient[0]) + const recipientIdentifier = firstSegment.includes('@') ? firstSegment.split('@')[0] : firstSegment + const isAddressRecipient = isAddress(recipientIdentifier) + const isEnsRecipient = recipientIdentifier.endsWith('.eth') - // reset payment state on unmount - useEffect(() => { - return () => { - dispatch(paymentActions.resetPaymentState()) - } - }, []) + // semantic request flow with chargeId (confirm view) + if (chargeIdFromUrl) { + return + } - if (error) { - return ( -
- -
- ) + // semantic request for address/ens recipients + if (isAddressRecipient || isEnsRecipient) { + return } - // show loading until URL is parsed and req/charge data is loaded or daimo payment is processing - const isLoading = - !isUrlParsed || (chargeId && !chargeDetails) || isRequestDetailsFetching || isDaimoPaymentProcessing + // semantic request for username with amount or chain specified + // handles: // or /@/ + const hasAmountSegment = recipient.length > 1 + const hasChainSpecified = firstSegment.includes('@') - if (isLoading) { - return ( -
- -
- ) + if (hasAmountSegment || hasChainSpecified) { + return } - // render request fulfilment bank flow manager - if (showRequestFulfilmentBankFlowManager) { - return + // public profile for username without payment data + // handles: / + const username = recipientIdentifier + const handleSendClick = () => { + router.push(`/send/${username}`) } - // render PUBLIC_PROFILE view - if ( - currentView === 'PUBLIC_PROFILE' && - parsedPaymentData?.recipient?.recipientType === 'USERNAME' && - !isExternalWalletFlow - ) { - const username = parsedPaymentData.recipient.identifier - const handleSendClick = () => { - router.push(`/send/${username}`) - } - return ( -
+ return ( + +
- ) - } - - return ( -
- {currentView === 'INITIAL' && ( -
- setCurrencyAmount(value || '')} - currencyAmount={currencyAmount} - showRequestPotInitialView={!!requestId} - /> -
- {!requestId && showActionList && ( - - )} -
-
- )} - {currentView === 'CONFIRM' && ( - - )} - {currentView === 'STATUS' && ( - <> - {isDrawerOpen && selectedTransaction?.id === transactionForDrawer?.id ? ( -
- - -
- ) : ( - - )} - - )} - - {/* Show only to guest users */} - {!user && !isFetchingUser && } -
+
) } - -const getDefaultError: (isUser: boolean) => ValidationErrorViewProps = (isUser) => ({ - title: 'Invalid Payment URL!', - message: 'They payment you are trying to access is invalid. Please check the URL and try again.', - buttonText: isUser ? 'Go to home' : 'Create your Peanut Wallet', - redirectTo: isUser ? '/home' : '/setup', -}) - -function getErrorProps({ - error, - isUser, - recipient, -}: { - error: ParseUrlError - isUser: boolean - recipient: string[] -}): ValidationErrorViewProps { - const username = recipient[0] || 'unknown' - - switch (error.message) { - case EParseUrlError.INVALID_RECIPIENT: - return { - title: `We don't know any @${username}`, - message: 'Are you sure you clicked on the right link?', - buttonText: 'Go back to home', - redirectTo: '/home', - showLearnMore: false, - supportMessageTemplate: 'I clicked on this link but got an error: {url}', - } - case EParseUrlError.INVALID_CHAIN: - return { - title: 'Invalid Chain', - message: 'You can pay the recipient in their preferred chain', - buttonText: 'Pay them in their preferred chain', - redirectTo: `/${error.recipient}`, - } - case EParseUrlError.INVALID_TOKEN: - return { - title: 'Invalid Token', - message: 'You can pay the recipient in their preferred token', - buttonText: 'Pay them in their preferred token', - redirectTo: `/${error.recipient}`, - } - case EParseUrlError.INVALID_AMOUNT: - return { - title: 'Invalid Amount', - message: 'Please check the url and try again', - buttonText: isUser ? 'Go to home' : 'Create your Peanut Wallet', - redirectTo: isUser ? '/home' : '/setup', - } - case EParseUrlError.INVALID_URL_FORMAT: - default: - return getDefaultError(isUser) - } -} diff --git a/src/app/[...recipient]/error.tsx b/src/app/[...recipient]/error.tsx index 363fdd47d..3ff40afd8 100644 --- a/src/app/[...recipient]/error.tsx +++ b/src/app/[...recipient]/error.tsx @@ -1,13 +1,14 @@ 'use client' -import { Button, Card } from '@/components/0_Bruddle' import { useEffect } from 'react' import { useRouter } from 'next/navigation' -import { useSupportModalContext } from '@/context/SupportModalContext' +import { useModalsContext } from '@/context/ModalsContext' +import { Button } from '@/components/0_Bruddle/Button' +import { Card } from '@/components/0_Bruddle/Card' export default function PaymentError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { const router = useRouter() - const { setIsSupportModalOpen } = useSupportModalContext() + const { setIsSupportModalOpen } = useModalsContext() useEffect(() => { console.error(error) diff --git a/src/app/[...recipient]/page.tsx b/src/app/[...recipient]/page.tsx index 64b5c73df..fe535e751 100644 --- a/src/app/[...recipient]/page.tsx +++ b/src/app/[...recipient]/page.tsx @@ -2,9 +2,9 @@ import PageContainer from '@/components/0_Bruddle/PageContainer' import { use } from 'react' import PaymentPage from './client' import getOrigin from '@/lib/hosting/get-origin' -import { BASE_URL } from '@/constants' +import { BASE_URL } from '@/constants/general.consts' import { isAddress } from 'viem' -import { printableAddress, resolveAddressToUsername } from '@/utils' +import { printableAddress } from '@/utils/general.utils' import { chargesApi } from '@/services/charges' import { parseAmountAndToken } from '@/lib/url-parser/parser' import { notFound } from 'next/navigation' diff --git a/src/app/[...recipient]/payment-layout-wrapper.tsx b/src/app/[...recipient]/payment-layout-wrapper.tsx index 3850ca3e1..9968068d4 100644 --- a/src/app/[...recipient]/payment-layout-wrapper.tsx +++ b/src/app/[...recipient]/payment-layout-wrapper.tsx @@ -41,7 +41,7 @@ export default function PaymentLayoutWrapper({ children }: { children: React.Rea className={classNames( twMerge( 'flex-1 overflow-y-auto bg-background p-6 md:pb-6', - isUserLoggedIn ? 'pb-24' : 'pb-6' + isUserLoggedIn ? 'pb-24' : 'pb-4' ) )} > diff --git a/src/app/actions.ts b/src/app/actions.ts deleted file mode 100644 index 6aacf0936..000000000 --- a/src/app/actions.ts +++ /dev/null @@ -1,113 +0,0 @@ -'use server' - -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry } from '@/utils' -import { cookies } from 'next/headers' -import webpush from 'web-push' - -const updateSubscription = async ({ userId, pushSubscriptionId }: { userId: string; pushSubscriptionId: string }) => { - const cookieStore = cookies() - const jwtToken = (await cookieStore).get('jwt-token')?.value - - return await fetchWithSentry(`${PEANUT_API_URL}/update-user`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${jwtToken}`, - 'api-key': process.env.PEANUT_API_KEY!, - }, - body: JSON.stringify({ - userId, - pushSubscriptionId, - }), - }) -} - -// add validation for VAPID environment variables -const validateVapidEnv = () => { - const vapidSubject = process.env.NEXT_PUBLIC_VAPID_SUBJECT - const vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY - const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY - - if (!vapidSubject || !vapidPublicKey || !vapidPrivateKey) { - throw new Error('VAPID environment variables are not set. Please check your environment configuration.') - } - - return { vapidSubject, vapidPublicKey, vapidPrivateKey } -} - -// initialize webpush with try-catch -// NOTE: This is legacy code - we now use OneSignal for push notifications -// TODO: Remove this entire file once PushProvider is fully migrated to OneSignal -try { - const { vapidSubject, vapidPublicKey, vapidPrivateKey } = validateVapidEnv() - - webpush.setVapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey) -} catch (error) { - // Silently fail in test environment to avoid breaking tests - if (process.env.NODE_ENV !== 'test') { - console.error('Failed to initialize web push:', error) - // in development, provide more helpful error message - if (process.env.NODE_ENV === 'development') { - console.info(` - Please ensure you have the following environment variables set: - - NEXT_PUBLIC_VAPID_SUBJECT (usually a mailto: URL) - - NEXT_PUBLIC_VAPID_PUBLIC_KEY - - VAPID_PRIVATE_KEY - `) - console.log('VAPID Subject:', process.env.NEXT_PUBLIC_VAPID_SUBJECT) - } - } -} - -let subscription: webpush.PushSubscription | null = null - -export async function subscribeUser( - userId: string, - sub: { - endpoint: string - keys: { - p256dh: string - auth: string - } - } -) { - try { - await updateSubscription({ - userId, - pushSubscriptionId: JSON.stringify(sub), - }) - - return { success: true } - } catch (error) { - console.error('Error updating subscription:', error) - return { success: false, error: 'Failed to update subscription' } - } -} - -export async function unsubscribeUser() { - subscription = null - // In a production environment, you would want to remove the subscription from the database - // For example: await db.subscriptions.delete({ where: { ... } }) - return { success: true } -} - -export async function sendNotification( - sub: webpush.PushSubscription, - { message, title }: { message: string; title: string } -) { - try { - await webpush.sendNotification( - sub, - JSON.stringify({ - title, - message, - icon: '/icons/icon-192x192-beta.png', - }) - ) - return { success: true } - } catch (error) { - console.error('Error sending push notification:', error) - return { success: false, error: 'Failed to send notification' } - } -} diff --git a/src/app/actions/bridge/get-customer.ts b/src/app/actions/bridge/get-customer.ts index 66a47b757..94e5fccf5 100644 --- a/src/app/actions/bridge/get-customer.ts +++ b/src/app/actions/bridge/get-customer.ts @@ -1,8 +1,8 @@ 'use server' import { unstable_cache } from 'next/cache' -import { PEANUT_API_KEY, PEANUT_API_URL } from '@/constants' import { countryData } from '@/components/AddMoney/consts' +import { PEANUT_API_KEY, PEANUT_API_URL } from '@/constants/general.consts' type BridgeCustomer = { id: string diff --git a/src/app/actions/claimLinks.ts b/src/app/actions/claimLinks.ts index c56662415..e3feeeffe 100644 --- a/src/app/actions/claimLinks.ts +++ b/src/app/actions/claimLinks.ts @@ -6,8 +6,8 @@ import { getContract } from 'viem' import { getPublicClient, type ChainId } from '@/app/actions/clients' import { fetchTokenDetails } from '@/app/actions/tokens' -import { getLinkFromReceipt } from '@/utils' -import { PEANUT_WALLET_CHAIN } from '@/constants' +import { getLinkFromReceipt } from '@/utils/general.utils' +import { PEANUT_WALLET_CHAIN } from '@/constants/zerodev.consts' export const getLinkDetails = unstable_cache( async (link: string): Promise => { diff --git a/src/app/actions/clients.ts b/src/app/actions/clients.ts index 1cf663731..126dfb980 100644 --- a/src/app/actions/clients.ts +++ b/src/app/actions/clients.ts @@ -1,8 +1,9 @@ +import { rpcUrls } from '@/constants/general.consts' +import { BUNDLER_URL, PAYMASTER_URL, PEANUT_WALLET_CHAIN } from '@/constants/zerodev.consts' import type { PublicClient, Chain, Transport } from 'viem' import { createPublicClient, http, extractChain, fallback } from 'viem' import * as chains from 'viem/chains' - -import { PUBLIC_CLIENTS_BY_CHAIN, rpcUrls } from '@/constants' +import { arbitrum, mainnet, base, linea } from 'viem/chains' const allChains = Object.values(chains) export type ChainId = (typeof allChains)[number]['id'] @@ -13,7 +14,6 @@ export type ChainId = (typeof allChains)[number]['id'] * @see https://viem.sh/docs/clients/transports/fallback#fallback-transport */ export function getTransportWithFallback(chainId: ChainId): Transport { - // Handle circular dependency during module initialization (e.g., in tests) const providerUrls = rpcUrls?.[chainId] if (!providerUrls || !Array.isArray(providerUrls) || providerUrls.length === 0) { // If no premium providers are configured, viem will use a default one @@ -28,6 +28,85 @@ export function getTransportWithFallback(chainId: ChainId): Transport { ) } +const ZERODEV_V3_URL = process.env.NEXT_PUBLIC_ZERO_DEV_RECOVERY_BUNDLER_URL +const zerodevV3Url = (chainId: number | string) => `${ZERODEV_V3_URL}/chain/${chainId}` + +/** + * This is a mapping of chain ID to the public client and chain details + * This is for the standard chains supported in the app. Arbitrum is always included + * as it's the primary wallet chain. Additional chains (mainnet, base, linea) are only + * included if NEXT_PUBLIC_ZERO_DEV_RECOVERY_BUNDLER_URL is configured. + * Note: PUBLIC_CLIENTS_BY_CHAIN and peanutPublicClient are now exported from here to avoid circular dependencies + */ +export const PUBLIC_CLIENTS_BY_CHAIN: Record< + string, + { + client: PublicClient + chain: Chain + bundlerUrl: string + paymasterUrl: string + } +> = { + // Arbitrum (primary wallet chain - always included) + [arbitrum.id]: { + client: createPublicClient({ + transport: getTransportWithFallback(arbitrum.id), + chain: arbitrum, + pollingInterval: 500, + }), + chain: PEANUT_WALLET_CHAIN, + bundlerUrl: BUNDLER_URL, + paymasterUrl: PAYMASTER_URL, + }, +} + +// Conditionally add recovery chains if env var is configured +if (ZERODEV_V3_URL) { + const mainnetUrl = zerodevV3Url(mainnet.id) + if (mainnetUrl) { + PUBLIC_CLIENTS_BY_CHAIN[mainnet.id] = { + client: createPublicClient({ + transport: getTransportWithFallback(mainnet.id), + chain: mainnet, + pollingInterval: 12000, + }), + chain: mainnet, + bundlerUrl: mainnetUrl, + paymasterUrl: mainnetUrl, + } + } + + const baseUrl = zerodevV3Url(base.id) + if (baseUrl) { + PUBLIC_CLIENTS_BY_CHAIN[base.id] = { + client: createPublicClient({ + transport: getTransportWithFallback(base.id), + chain: base, + pollingInterval: 2000, + }) as PublicClient, + chain: base, + bundlerUrl: baseUrl, + paymasterUrl: baseUrl, + } + } + + const lineaUrl = zerodevV3Url(linea.id) + if (lineaUrl) { + PUBLIC_CLIENTS_BY_CHAIN[linea.id] = { + client: createPublicClient({ + transport: getTransportWithFallback(linea.id), + chain: linea, + pollingInterval: 3000, + }), + chain: linea, + bundlerUrl: lineaUrl, + paymasterUrl: lineaUrl, + } + } +} + +export const peanutPublicClient = PUBLIC_CLIENTS_BY_CHAIN[PEANUT_WALLET_CHAIN.id].client + export function getPublicClient(chainId: ChainId): PublicClient { let client: PublicClient | undefined = PUBLIC_CLIENTS_BY_CHAIN[chainId]?.client if (client) return client diff --git a/src/app/actions/ens.ts b/src/app/actions/ens.ts index e79f1162d..6e9998134 100644 --- a/src/app/actions/ens.ts +++ b/src/app/actions/ens.ts @@ -1,7 +1,7 @@ 'use server' import { unstable_cache } from 'next/cache' -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { PEANUT_API_URL } from '@/constants/general.consts' const API_KEY = process.env.PEANUT_API_KEY! diff --git a/src/app/actions/exchange-rate.ts b/src/app/actions/exchange-rate.ts index e7c7748a1..0ed97f55b 100644 --- a/src/app/actions/exchange-rate.ts +++ b/src/app/actions/exchange-rate.ts @@ -1,6 +1,6 @@ 'use server' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { AccountType } from '@/interfaces' const API_KEY = process.env.PEANUT_API_KEY! diff --git a/src/app/actions/external-accounts.ts b/src/app/actions/external-accounts.ts index c210a6f30..bc47f33bb 100644 --- a/src/app/actions/external-accounts.ts +++ b/src/app/actions/external-accounts.ts @@ -1,6 +1,6 @@ 'use server' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { type AddBankAccountPayload } from './types/users.types' import { type IBridgeAccount } from '@/interfaces' diff --git a/src/app/actions/history.ts b/src/app/actions/history.ts index 0735e84b8..fc6d163ef 100644 --- a/src/app/actions/history.ts +++ b/src/app/actions/history.ts @@ -2,8 +2,8 @@ import { EHistoryEntryType, completeHistoryEntry } from '@/utils/history.utils' import type { HistoryEntry } from '@/utils/history.utils' -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry } from '@/utils' +import { PEANUT_API_URL } from '@/constants/general.consts' +import { fetchWithSentry } from '@/utils/sentry.utils' /** * Fetches a single history entry from the API. This is used for receipts diff --git a/src/app/actions/invites.ts b/src/app/actions/invites.ts index 2eabe0e99..eff8d6d0e 100644 --- a/src/app/actions/invites.ts +++ b/src/app/actions/invites.ts @@ -1,7 +1,7 @@ 'use server' -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { PEANUT_API_URL } from '@/constants/general.consts' const API_KEY = process.env.PEANUT_API_KEY! diff --git a/src/app/actions/offramp.ts b/src/app/actions/offramp.ts index ed81a42b0..fd9c96b6e 100644 --- a/src/app/actions/offramp.ts +++ b/src/app/actions/offramp.ts @@ -2,7 +2,7 @@ import { cookies } from 'next/headers' import { type TCreateOfframpRequest } from '../../services/services.types' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' const API_KEY = process.env.PEANUT_API_KEY! diff --git a/src/app/actions/onramp.ts b/src/app/actions/onramp.ts index 5c28ffab8..2ed5402d8 100644 --- a/src/app/actions/onramp.ts +++ b/src/app/actions/onramp.ts @@ -1,7 +1,7 @@ 'use server' import { cookies } from 'next/headers' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { type CountryData } from '@/components/AddMoney/consts' import { getCurrencyConfig } from '@/utils/bridge.utils' import { getCurrencyPrice } from '@/app/actions/currency' diff --git a/src/app/actions/squid.ts b/src/app/actions/squid.ts index 23c2d9390..38b7777c0 100644 --- a/src/app/actions/squid.ts +++ b/src/app/actions/squid.ts @@ -3,7 +3,7 @@ import { getSquidChains, getSquidTokens } from '@squirrel-labs/peanut-sdk' import { unstable_cache } from 'next/cache' import { interfaces } from '@squirrel-labs/peanut-sdk' -import { supportedPeanutChains } from '@/constants' +import { supportedPeanutChains } from '@/constants/general.consts' const supportedByPeanut = (chain: interfaces.ISquidChain): boolean => 'evm' === chain.chainType && diff --git a/src/app/actions/tokens.ts b/src/app/actions/tokens.ts index d86f1da81..d84632811 100644 --- a/src/app/actions/tokens.ts +++ b/src/app/actions/tokens.ts @@ -1,11 +1,18 @@ 'use server' import { unstable_cache } from 'next/cache' -import { fetchWithSentry, isAddressZero, estimateIfIsStableCoinFromPrice } from '@/utils' +import { + isAddressZero, + estimateIfIsStableCoinFromPrice, + getTokenDetails, + isStableCoin, + areEvmAddressesEqual, +} from '@/utils/general.utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { NATIVE_TOKEN_ADDRESS } from '@/utils/token.utils' import { type ITokenPriceData } from '@/interfaces' import { parseAbi, formatUnits } from 'viem' import { type ChainId, getPublicClient } from '@/app/actions/clients' import type { Address, Hex } from 'viem' -import { getTokenDetails, isStableCoin, areEvmAddressesEqual, NATIVE_TOKEN_ADDRESS } from '@/utils' import { type IUserBalance } from '@/interfaces' type IMobulaMarketData = { diff --git a/src/app/actions/users.ts b/src/app/actions/users.ts index 8c6180bc9..31be9651e 100644 --- a/src/app/actions/users.ts +++ b/src/app/actions/users.ts @@ -1,12 +1,12 @@ 'use server' -import { PEANUT_API_URL } from '@/constants' import { type ApiUser } from '@/services/users' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { cookies } from 'next/headers' import { type AddBankAccountPayload, BridgeEndorsementType, type InitiateKycResponse } from './types/users.types' import { type User } from '@/interfaces' import { type ContactsResponse } from '@/interfaces' +import { PEANUT_API_URL } from '@/constants/general.consts' const API_KEY = process.env.PEANUT_API_KEY! @@ -168,6 +168,7 @@ export async function trackDaimoDepositTransactionHash({ export async function getContacts(params: { limit: number offset: number + search?: string }): Promise<{ data?: ContactsResponse; error?: string }> { const cookieStore = cookies() const jwtToken = (await cookieStore).get('jwt-token')?.value @@ -182,6 +183,11 @@ export async function getContacts(params: { offset: params.offset.toString(), }) + // add search param if provided + if (params.search?.trim()) { + queryParams.append('search', params.search.trim()) + } + const response = await fetchWithSentry(`${PEANUT_API_URL}/users/contacts?${queryParams}`, { method: 'GET', headers: { diff --git a/src/app/api/apple-app-site-association/route.ts b/src/app/api/apple-app-site-association/route.ts index 159aac016..28d4d279b 100644 --- a/src/app/api/apple-app-site-association/route.ts +++ b/src/app/api/apple-app-site-association/route.ts @@ -1,7 +1,7 @@ -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' +import { PEANUT_API_URL } from '@/constants/general.consts' export const dynamic = 'force-dynamic' export async function GET(_request: NextRequest) { diff --git a/src/app/api/assetLinks/route.ts b/src/app/api/assetLinks/route.ts index 949c2f6bb..59df4c891 100644 --- a/src/app/api/assetLinks/route.ts +++ b/src/app/api/assetLinks/route.ts @@ -1,8 +1,8 @@ import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' -import { PEANUT_API_URL } from '@/constants' +import { PEANUT_API_URL } from '@/constants/general.consts' export const dynamic = 'force-dynamic' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' export async function GET(request: NextRequest) { const response = await fetchWithSentry(`${PEANUT_API_URL}/assetLinks.json`) diff --git a/src/app/api/health/backend/route.ts b/src/app/api/health/backend/route.ts index 3d1333455..10bf1e40d 100644 --- a/src/app/api/health/backend/route.ts +++ b/src/app/api/health/backend/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server' -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { PEANUT_API_URL } from '@/constants/general.consts' export const dynamic = 'force-dynamic' export const revalidate = 0 diff --git a/src/app/api/health/justaname/route.ts b/src/app/api/health/justaname/route.ts index 1d9db5588..54912392f 100644 --- a/src/app/api/health/justaname/route.ts +++ b/src/app/api/health/justaname/route.ts @@ -1,4 +1,4 @@ -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { NextResponse } from 'next/server' const JUSTANAME_API_URL = 'https://api.justaname.id' diff --git a/src/app/api/health/mobula/route.ts b/src/app/api/health/mobula/route.ts index 36ba0279b..f9a86ff67 100644 --- a/src/app/api/health/mobula/route.ts +++ b/src/app/api/health/mobula/route.ts @@ -1,4 +1,4 @@ -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { NextResponse } from 'next/server' const MOBULA_API_URL = process.env.MOBULA_API_URL! diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts index fba4ef5f6..89c696bd0 100644 --- a/src/app/api/health/route.ts +++ b/src/app/api/health/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server' -import { fetchWithSentry } from '@/utils' -import { SELF_URL } from '@/constants' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { SELF_URL } from '@/constants/general.consts' export const dynamic = 'force-dynamic' export const revalidate = 0 diff --git a/src/app/api/health/rpc/route.ts b/src/app/api/health/rpc/route.ts index 12a68e998..3f06a4196 100644 --- a/src/app/api/health/rpc/route.ts +++ b/src/app/api/health/rpc/route.ts @@ -1,4 +1,4 @@ -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { NextResponse } from 'next/server' import { rpcUrls } from '@/constants/general.consts' diff --git a/src/app/api/health/squid/route.ts b/src/app/api/health/squid/route.ts index 6607db9c6..08d1c44f7 100644 --- a/src/app/api/health/squid/route.ts +++ b/src/app/api/health/squid/route.ts @@ -1,6 +1,6 @@ -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { NextResponse } from 'next/server' -import { SQUID_API_URL, SQUID_INTEGRATOR_ID, SQUID_INTEGRATOR_ID_WITHOUT_CORAL } from '@/constants' +import { SQUID_INTEGRATOR_ID_WITHOUT_CORAL, SQUID_INTEGRATOR_ID, SQUID_API_URL } from '@/constants/general.consts' /** * Health check for Squid API diff --git a/src/app/api/health/zerodev/route.ts b/src/app/api/health/zerodev/route.ts index e9d3531cb..de604d3dc 100644 --- a/src/app/api/health/zerodev/route.ts +++ b/src/app/api/health/zerodev/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from 'next/server' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' /** * ZeroDev health check endpoint diff --git a/src/app/api/og/route.tsx b/src/app/api/og/route.tsx index 8d67c6076..2d8b6d09a 100644 --- a/src/app/api/og/route.tsx +++ b/src/app/api/og/route.tsx @@ -4,13 +4,14 @@ import { NextResponse, type NextRequest } from 'next/server' import { type PaymentLink } from '@/interfaces' import { promises as fs } from 'fs' import path from 'path' -import { BASE_URL } from '@/constants' import getOrigin from '@/lib/hosting/get-origin' import { ReceiptCardOG } from '@/components/og/ReceiptCardOG' -import { printableAddress, resolveAddressToUsername } from '@/utils' +import { printableAddress } from '@/utils/general.utils' +import { resolveAddressToUsername } from '@/utils/ens.utils' import { isAddress } from 'viem' import { ProfileCardOG } from '@/components/og/ProfileCardOG' import { InviteCardOG } from '@/components/og/InviteCardOG' +import { BASE_URL } from '@/constants/general.consts' export const runtime = 'nodejs' //node.js instead of edge! diff --git a/src/app/api/peanut/get-attachment-info/route.ts b/src/app/api/peanut/get-attachment-info/route.ts index 1c0060158..e9cbfe7cc 100644 --- a/src/app/api/peanut/get-attachment-info/route.ts +++ b/src/app/api/peanut/get-attachment-info/route.ts @@ -1,8 +1,5 @@ import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' -import { getRawParamsFromLink, generateKeysFromString } from '@squirrel-labs/peanut-sdk' // Adjust the import paths according to your project structure -import * as consts from '@/constants' -import { fetchWithSentry } from '@/utils' export async function POST(request: NextRequest) { //TODO: enable if we have attachments again, using /send-link instead of diff --git a/src/app/api/peanut/get-user-stats/route.ts b/src/app/api/peanut/get-user-stats/route.ts index b9e063af9..a7c2ad770 100644 --- a/src/app/api/peanut/get-user-stats/route.ts +++ b/src/app/api/peanut/get-user-stats/route.ts @@ -1,14 +1,14 @@ // pages/api/get-user-stats.ts import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' -import * as consts from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { PEANUT_API_URL } from '@/constants/general.consts' export async function POST(request: NextRequest) { try { const { address } = await request.json() - const response = await fetchWithSentry(`${consts.PEANUT_API_URL}/get-user-stats`, { + const response = await fetchWithSentry(`${PEANUT_API_URL}/get-user-stats`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/src/app/api/peanut/iban/validate-bank-account-number/route.ts b/src/app/api/peanut/iban/validate-bank-account-number/route.ts index 5e8e372f0..134301de6 100644 --- a/src/app/api/peanut/iban/validate-bank-account-number/route.ts +++ b/src/app/api/peanut/iban/validate-bank-account-number/route.ts @@ -1,7 +1,7 @@ import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' -import * as consts from '@/constants' -import { fetchWithSentry } from '@/utils' +import { PEANUT_API_URL } from '@/constants/general.consts' +import { fetchWithSentry } from '@/utils/sentry.utils' export async function POST(request: NextRequest) { try { @@ -12,7 +12,7 @@ export async function POST(request: NextRequest) { return new NextResponse('Bad Request: missing required parameters', { status: 400 }) } - const response = await fetchWithSentry(`${consts.PEANUT_API_URL}/validate-bank-account-number`, { + const response = await fetchWithSentry(`${PEANUT_API_URL}/validate-bank-account-number`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/iban/validate-bic/route.ts b/src/app/api/peanut/iban/validate-bic/route.ts index cd08c39f1..b70604410 100644 --- a/src/app/api/peanut/iban/validate-bic/route.ts +++ b/src/app/api/peanut/iban/validate-bic/route.ts @@ -1,7 +1,7 @@ import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' -import * as consts from '@/constants' -import { fetchWithSentry } from '@/utils' +import { PEANUT_API_URL } from '@/constants/general.consts' +import { fetchWithSentry } from '@/utils/sentry.utils' export async function POST(request: NextRequest) { try { @@ -12,7 +12,7 @@ export async function POST(request: NextRequest) { return new NextResponse('Bad Request: missing required parameters', { status: 400 }) } - const response = await fetchWithSentry(`${consts.PEANUT_API_URL}/is-valid-bic`, { + const response = await fetchWithSentry(`${PEANUT_API_URL}/is-valid-bic`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/user/add-account/route.ts b/src/app/api/peanut/user/add-account/route.ts index 4073cc8f0..a71ecff8e 100644 --- a/src/app/api/peanut/user/add-account/route.ts +++ b/src/app/api/peanut/user/add-account/route.ts @@ -1,8 +1,8 @@ -import * as consts from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { cookies } from 'next/headers' import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' +import { PEANUT_API_URL } from '@/constants/general.consts' export async function POST(request: NextRequest) { try { @@ -18,7 +18,7 @@ export async function POST(request: NextRequest) { return new NextResponse('Bad Request: Missing required fields', { status: 400 }) } - const response = await fetchWithSentry(`${consts.PEANUT_API_URL}/add-account`, { + const response = await fetchWithSentry(`${PEANUT_API_URL}/add-account`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/user/create-user/route.ts b/src/app/api/peanut/user/create-user/route.ts index fec7db634..271ca4599 100644 --- a/src/app/api/peanut/user/create-user/route.ts +++ b/src/app/api/peanut/user/create-user/route.ts @@ -1,7 +1,7 @@ import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' -import * as consts from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { PEANUT_API_URL } from '@/constants/general.consts' export async function POST(request: NextRequest) { try { @@ -14,7 +14,7 @@ export async function POST(request: NextRequest) { return new NextResponse('Bad Request: Missing required fields', { status: 400 }) } - const response = await fetchWithSentry(`${consts.PEANUT_API_URL}/user/create`, { + const response = await fetchWithSentry(`${PEANUT_API_URL}/user/create`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/user/fetch-user/route.ts b/src/app/api/peanut/user/fetch-user/route.ts index 67ae6a28e..93ca9b74f 100644 --- a/src/app/api/peanut/user/fetch-user/route.ts +++ b/src/app/api/peanut/user/fetch-user/route.ts @@ -1,7 +1,7 @@ import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' -import * as consts from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { PEANUT_API_URL } from '@/constants/general.consts' export const dynamic = 'force-dynamic' // Explicitly mark the route as dynamic @@ -17,7 +17,7 @@ export async function GET(request: NextRequest) { const uniqueKey = `${Date.now()}-${accountIdentifier}` const response = await fetchWithSentry( - `${consts.PEANUT_API_URL}/user/fetch?accountIdentifier=${accountIdentifier}&uniqueKey=${uniqueKey}`, + `${PEANUT_API_URL}/user/fetch?accountIdentifier=${accountIdentifier}&uniqueKey=${uniqueKey}`, { method: 'GET', headers: { diff --git a/src/app/api/peanut/user/get-jwt-token/route.ts b/src/app/api/peanut/user/get-jwt-token/route.ts index 01ee00810..76dc5dbb4 100644 --- a/src/app/api/peanut/user/get-jwt-token/route.ts +++ b/src/app/api/peanut/user/get-jwt-token/route.ts @@ -1,7 +1,7 @@ -import * as consts from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { cookies } from 'next/headers' import { NextRequest, NextResponse } from 'next/server' +import { PEANUT_API_URL } from '@/constants/general.consts' export async function POST(request: NextRequest) { const { signature, message } = await request.json() @@ -12,7 +12,7 @@ export async function POST(request: NextRequest) { } try { - const response = await fetchWithSentry(`${consts.PEANUT_API_URL}/get-token`, { + const response = await fetchWithSentry(`${PEANUT_API_URL}/get-token`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/user/get-user-from-cookie/route.ts b/src/app/api/peanut/user/get-user-from-cookie/route.ts index e3ad7aefe..4ee0bb93b 100644 --- a/src/app/api/peanut/user/get-user-from-cookie/route.ts +++ b/src/app/api/peanut/user/get-user-from-cookie/route.ts @@ -1,5 +1,5 @@ -import * as consts from '@/constants' -import { fetchWithSentry } from '@/utils' +import { PEANUT_API_URL } from '@/constants/general.consts' +import { fetchWithSentry } from '@/utils/sentry.utils' import { cookies } from 'next/headers' import { NextRequest, NextResponse } from 'next/server' @@ -12,7 +12,7 @@ export async function GET(_request: NextRequest) { return new NextResponse('Bad Request: missing required parameters', { status: 400 }) } try { - const response = await fetchWithSentry(`${consts.PEANUT_API_URL}/get-user`, { + const response = await fetchWithSentry(`${PEANUT_API_URL}/get-user`, { method: 'POST', headers: { Authorization: `Bearer ${token.value}`, diff --git a/src/app/api/peanut/user/get-user-id/route.ts b/src/app/api/peanut/user/get-user-id/route.ts index 9795db0fe..dc3c1df14 100644 --- a/src/app/api/peanut/user/get-user-id/route.ts +++ b/src/app/api/peanut/user/get-user-id/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import * as consts from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { PEANUT_API_URL } from '@/constants/general.consts' export async function POST(request: NextRequest) { const { accountIdentifier } = await request.json() @@ -11,7 +11,7 @@ export async function POST(request: NextRequest) { } try { - const response = await fetchWithSentry(`${consts.PEANUT_API_URL}/get-user-id`, { + const response = await fetchWithSentry(`${PEANUT_API_URL}/get-user-id`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/user/get-user-salt/route.ts b/src/app/api/peanut/user/get-user-salt/route.ts index fd340cda3..f336eb052 100644 --- a/src/app/api/peanut/user/get-user-salt/route.ts +++ b/src/app/api/peanut/user/get-user-salt/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import * as consts from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { PEANUT_API_URL } from '@/constants/general.consts' export async function POST(request: NextRequest) { const { email } = await request.json() @@ -11,7 +11,7 @@ export async function POST(request: NextRequest) { } try { - const response = await fetchWithSentry(`${consts.PEANUT_API_URL}/get-user-salt`, { + const response = await fetchWithSentry(`${PEANUT_API_URL}/get-user-salt`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/user/login-user/route.ts b/src/app/api/peanut/user/login-user/route.ts index 5fd2992c2..6f48f351c 100644 --- a/src/app/api/peanut/user/login-user/route.ts +++ b/src/app/api/peanut/user/login-user/route.ts @@ -1,5 +1,5 @@ -import * as consts from '@/constants' -import { fetchWithSentry } from '@/utils' +import { PEANUT_API_URL } from '@/constants/general.consts' +import { fetchWithSentry } from '@/utils/sentry.utils' import { cookies } from 'next/headers' import { NextRequest, NextResponse } from 'next/server' @@ -12,7 +12,7 @@ export async function POST(request: NextRequest) { } try { - const response = await fetchWithSentry(`${consts.PEANUT_API_URL}/login-user`, { + const response = await fetchWithSentry(`${PEANUT_API_URL}/login-user`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/user/register-user/route.ts b/src/app/api/peanut/user/register-user/route.ts index 02dce681c..c93f0e027 100644 --- a/src/app/api/peanut/user/register-user/route.ts +++ b/src/app/api/peanut/user/register-user/route.ts @@ -1,7 +1,7 @@ -import * as consts from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { cookies } from 'next/headers' import { NextRequest, NextResponse } from 'next/server' +import { PEANUT_API_URL } from '@/constants/general.consts' export async function POST(request: NextRequest) { const { email, hash, salt, fullName } = await request.json() @@ -12,7 +12,7 @@ export async function POST(request: NextRequest) { } try { - const response = await fetchWithSentry(`${consts.PEANUT_API_URL}/register-user`, { + const response = await fetchWithSentry(`${PEANUT_API_URL}/register-user`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/user/submit-profile-photo/route.ts b/src/app/api/peanut/user/submit-profile-photo/route.ts index bddeaea41..6039aa190 100644 --- a/src/app/api/peanut/user/submit-profile-photo/route.ts +++ b/src/app/api/peanut/user/submit-profile-photo/route.ts @@ -1,7 +1,7 @@ -import * as consts from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { cookies } from 'next/headers' import { NextRequest, NextResponse } from 'next/server' +import { PEANUT_API_URL } from '@/constants/general.consts' export async function POST(request: NextRequest) { const formData = await request.formData() @@ -23,7 +23,7 @@ export async function POST(request: NextRequest) { const apiFormData = new FormData() apiFormData.append('file', file) - const response = await fetchWithSentry(`${consts.PEANUT_API_URL}/submit-profile-photo`, { + const response = await fetchWithSentry(`${PEANUT_API_URL}/submit-profile-photo`, { method: 'POST', headers: { Authorization: `Bearer ${token.value}`, diff --git a/src/app/api/peanut/user/update-user/route.ts b/src/app/api/peanut/user/update-user/route.ts index a112ef6b1..15d1289f4 100644 --- a/src/app/api/peanut/user/update-user/route.ts +++ b/src/app/api/peanut/user/update-user/route.ts @@ -1,7 +1,8 @@ -import * as consts from '@/constants' -import { fetchWithSentry, type BridgeKycStatus } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import type { BridgeKycStatus } from '@/utils/bridge-accounts.utils' import { cookies } from 'next/headers' import { NextRequest, NextResponse } from 'next/server' +import { PEANUT_API_URL } from '@/constants/general.consts' type UserPayload = { userId: string @@ -50,7 +51,7 @@ export async function POST(request: NextRequest) { payload.fullName = fullName } - const response = await fetchWithSentry(`${consts.PEANUT_API_URL}/update-user`, { + const response = await fetchWithSentry(`${PEANUT_API_URL}/update-user`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/proxy/[...slug]/route.ts b/src/app/api/proxy/[...slug]/route.ts index 2b5ffdcc6..773951e2f 100644 --- a/src/app/api/proxy/[...slug]/route.ts +++ b/src/app/api/proxy/[...slug]/route.ts @@ -1,6 +1,6 @@ -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { NextRequest, NextResponse } from 'next/server' +import { PEANUT_API_URL } from '@/constants/general.consts' export const maxDuration = 300 // vercel timeout diff --git a/src/app/api/proxy/get/[...slug]/route.ts b/src/app/api/proxy/get/[...slug]/route.ts index 5af77de02..70550e88f 100644 --- a/src/app/api/proxy/get/[...slug]/route.ts +++ b/src/app/api/proxy/get/[...slug]/route.ts @@ -1,6 +1,6 @@ -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { NextRequest, NextResponse } from 'next/server' +import { PEANUT_API_URL } from '@/constants/general.consts' export async function GET(request: NextRequest) { const separator = '/api/proxy/get/' diff --git a/src/app/api/proxy/patch/[...slug]/route.ts b/src/app/api/proxy/patch/[...slug]/route.ts index 8080d11c8..3ddccab83 100644 --- a/src/app/api/proxy/patch/[...slug]/route.ts +++ b/src/app/api/proxy/patch/[...slug]/route.ts @@ -1,6 +1,6 @@ -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { NextRequest, NextResponse } from 'next/server' +import { PEANUT_API_URL } from '@/constants/general.consts' export async function PATCH(request: NextRequest) { const separator = '/api/proxy/patch/' diff --git a/src/app/api/proxy/withFormData/[...slug]/route.ts b/src/app/api/proxy/withFormData/[...slug]/route.ts index 947216215..0b29016d8 100644 --- a/src/app/api/proxy/withFormData/[...slug]/route.ts +++ b/src/app/api/proxy/withFormData/[...slug]/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { PEANUT_API_URL } from '@/constants/general.consts' async function handleFormDataRequest(request: NextRequest, method: string) { const separator = '/api/proxy/withFormData/' diff --git a/src/app/api/recent-transactions/route.ts b/src/app/api/recent-transactions/route.ts index fc0183e7e..28f05d2cd 100644 --- a/src/app/api/recent-transactions/route.ts +++ b/src/app/api/recent-transactions/route.ts @@ -1,4 +1,4 @@ -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { NextRequest } from 'next/server' interface TransferDetails { diff --git a/src/app/api/send-discord-notification/route.ts b/src/app/api/send-discord-notification/route.ts index 535965cd5..42dc4e2cd 100644 --- a/src/app/api/send-discord-notification/route.ts +++ b/src/app/api/send-discord-notification/route.ts @@ -1,5 +1,5 @@ import type { NextRequest } from 'next/server' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' export async function POST(request: NextRequest) { try { diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx index 6a6fa124f..ad6f00809 100644 --- a/src/app/invite/page.tsx +++ b/src/app/invite/page.tsx @@ -1,8 +1,8 @@ import InvitesPage from '@/components/Invites/InvitesPage' -import { BASE_URL } from '@/constants' import getOrigin from '@/lib/hosting/get-origin' import { type Metadata } from 'next' import { validateInviteCode } from '../actions/invites' +import { BASE_URL } from '@/constants/general.consts' export const dynamic = 'force-dynamic' diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5880f53b1..73ec69d1c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,16 +1,42 @@ -import { ConsoleGreeting } from '@/components/Global/ConsoleGreeting' -import { ScreenOrientationLocker } from '@/components/Global/ScreenOrientationLocker' -import { TranslationSafeWrapper } from '@/components/Global/TranslationSafeWrapper' -import { PeanutProvider } from '@/config' -import { ContextProvider } from '@/context' -import { FooterVisibilityProvider } from '@/context/footerVisibility' +import { ClientProviders } from './ClientProviders' import { type Viewport } from 'next' import { Londrina_Solid, Roboto_Flex, Sniglet } from 'next/font/google' import localFont from 'next/font/local' import Script from 'next/script' import '../styles/globals.css' -import { generateMetadata } from './metadata' -import { PEANUT_API_URL } from '@/constants/general.consts' +import { PEANUT_API_URL, BASE_URL } from '@/constants/general.consts' +import { type Metadata } from 'next' + +const baseUrl = BASE_URL || 'https://peanut.me' + +export const metadata: Metadata = { + title: 'Peanut - Instant Global P2P Payments in Digital Dollars', + description: + 'Send and receive money instantly with Peanut - a fast, peer-to-peer payments app powered by digital dollars. Easily transfer funds across borders. Enjoy cheap, instant remittances and cash out to local banks without technical hassle.', + metadataBase: new URL(baseUrl), + icons: { icon: '/favicon.ico' }, + keywords: + 'peer-to-peer payments, send money instantly, request money, fast global transfers, remittances, digital dollar transfers, Latin America, Argentina, Brazil, P2P payments, crypto payments, stablecoin, digital dollars', + openGraph: { + type: 'website', + title: 'Peanut - Instant Global P2P Payments in Digital Dollars', + description: + 'Send and receive money instantly with Peanut - a fast, peer-to-peer payments app powered by digital dollars.', + url: baseUrl, + siteName: 'Peanut', + images: [{ url: '/metadata-img.png', width: 1200, height: 630, alt: 'Peanut' }], + }, + twitter: { + card: 'summary_large_image', + title: 'Peanut - Instant Global P2P Payments in Digital Dollars', + description: + 'Send and receive money instantly with Peanut - a fast, peer-to-peer payments app powered by digital dollars.', + images: ['/metadata-img.png'], + creator: '@PeanutProtocol', + site: '@PeanutProtocol', + }, + applicationName: process.env.NODE_ENV === 'development' ? 'Peanut Dev' : 'Peanut', +} const roboto = Roboto_Flex({ subsets: ['latin'], @@ -48,15 +74,6 @@ const robotoFlexBold = localFont({ variable: '--font-roboto-flex-bold', }) -export const metadata = generateMetadata({ - title: 'Peanut - Instant Global P2P Payments in Digital Dollars', - description: - 'Send and receive money instantly with Peanut - a fast, peer-to-peer payments app powered by digital dollars. Easily transfer funds across borders. Enjoy cheap, instant remittances and cash out to local banks without technical hassle.', - image: '/metadata-img.png', - keywords: - 'peer-to-peer payments, send money instantly, request money, fast global transfers, remittances, digital dollar transfers, Latin America, Argentina, Brazil, P2P payments, crypto payments, stablecoin, digital dollars', -}) - export const viewport: Viewport = { width: 'device-width', initialScale: 1, @@ -74,12 +91,13 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - {/* CRITICAL PATH: Optimize QR payment flow loading */} - {/* Prefetch /qr-pay route + DNS for Manteca API */} - + {/* DNS prefetch for API */} + {/* Prefetch /qr-pay route - disabled in dev to avoid 9s+ compile time */} + {process.env.NODE_ENV !== 'development' && } + {/* Service Worker Registration: Register early for offline support and caching */} {/* CRITICAL: Must run before React hydration to enable offline-first PWA */} {process.env.NODE_ENV !== 'development' && ( @@ -138,15 +156,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - - - - - - {children} - - - + {children} ) diff --git a/src/app/maintenance/page.tsx b/src/app/maintenance/page.tsx index e452cd745..7f1fe2def 100644 --- a/src/app/maintenance/page.tsx +++ b/src/app/maintenance/page.tsx @@ -1,6 +1,6 @@ 'use client' import chillPeanutAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_01.gif' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import Image from 'next/image' import { useRouter } from 'next/navigation' diff --git a/src/app/metadata.ts b/src/app/metadata.ts index 4832b7d44..c7aeb73c7 100644 --- a/src/app/metadata.ts +++ b/src/app/metadata.ts @@ -1,4 +1,4 @@ -import { BASE_URL } from '@/constants' +import { BASE_URL } from '@/constants/general.consts' import { type Metadata } from 'next' export function generateMetadata({ diff --git a/src/app/page.tsx b/src/app/page.tsx index a155a4d03..676202274 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -11,10 +11,10 @@ import { SendInSeconds, YourMoney, RegulatedRails, - TweetCarousel, } from '@/components/LandingPage' import Footer from '@/components/LandingPage/Footer' import Manteca from '@/components/LandingPage/Manteca' +import TweetCarousel from '@/components/LandingPage/TweetCarousel' import { useFooterVisibility } from '@/context/footerVisibility' import { useEffect, useState, useRef } from 'react' diff --git a/src/app/quests/[questId]/page.tsx b/src/app/quests/[questId]/page.tsx index 7e7033157..88b8654e0 100644 --- a/src/app/quests/[questId]/page.tsx +++ b/src/app/quests/[questId]/page.tsx @@ -8,7 +8,7 @@ import Image from 'next/image' import borderCloud from '@/assets/illustrations/border-cloud.svg' import { Star } from '@/assets' import { useEffect, useState, useCallback } from 'react' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { QuestLeaderboard } from '../components/QuestLeaderboard' import { UserRankCard } from '../components/UserRankCard' import { QUEST_CONFIG, getQuestStatus, type QuestId } from '../constants' diff --git a/src/app/quests/components/QuestsHero.tsx b/src/app/quests/components/QuestsHero.tsx index 8fe928430..4fbc2434a 100644 --- a/src/app/quests/components/QuestsHero.tsx +++ b/src/app/quests/components/QuestsHero.tsx @@ -8,7 +8,7 @@ import handPointing from '@/assets/illustrations/got-it-hand.svg' import { Star } from '@/assets' import { useRouter, useSearchParams } from 'next/navigation' import Image from 'next/image' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { QUEST_CONFIG, getQuestStatus } from '../constants' import { useAllQuestsLeaderboards } from '../hooks/useQuests' import { useAuth } from '@/context/authContext' diff --git a/src/app/quests/explore/page.tsx b/src/app/quests/explore/page.tsx index 1e6ef4059..cfaa2e94c 100644 --- a/src/app/quests/explore/page.tsx +++ b/src/app/quests/explore/page.tsx @@ -5,10 +5,9 @@ import { useRouter, useSearchParams } from 'next/navigation' import { motion } from 'framer-motion' import Image from 'next/image' import borderCloud from '@/assets/illustrations/border-cloud.svg' -import handPointing from '@/assets/illustrations/got-it-hand.svg' import { Star } from '@/assets' import { useEffect, useState, useMemo, useCallback } from 'react' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { QuestLeaderboard } from '../components/QuestLeaderboard' import { UserRankCard } from '../components/UserRankCard' import { QUEST_CONFIG, getQuestStatus } from '../constants' diff --git a/src/app/receipt/[entryId]/page.tsx b/src/app/receipt/[entryId]/page.tsx index 3f3a6e1d4..2741503c6 100644 --- a/src/app/receipt/[entryId]/page.tsx +++ b/src/app/receipt/[entryId]/page.tsx @@ -10,9 +10,10 @@ import { TransactionDetailsReceipt } from '@/components/TransactionDetails/Trans import NavHeader from '@/components/Global/NavHeader' import { generateMetadata as generateBaseMetadata } from '@/app/metadata' import { type Metadata } from 'next' -import { BASE_URL } from '@/constants' -import { formatAmount, formatCurrency, isStableCoin } from '@/utils' +import { BASE_URL } from '@/constants/general.consts' +import { formatAmount, formatCurrency, isStableCoin } from '@/utils/general.utils' import getOrigin from '@/lib/hosting/get-origin' +import PageContainer from '@/components/0_Bruddle/PageContainer' // Helper function to map transaction card type to OG image type function mapTransactionTypeToOGType(transactionType: string): 'send' | 'request' { @@ -186,13 +187,13 @@ export default async function ReceiptPage({ } const { transactionDetails } = mapTransactionDataForDrawer(entry) return ( -
+
-
+
-
+
) } diff --git a/src/app/robots.ts b/src/app/robots.ts index 2a5975684..d9d154d42 100644 --- a/src/app/robots.ts +++ b/src/app/robots.ts @@ -1,5 +1,5 @@ import type { MetadataRoute } from 'next' -import { BASE_URL } from '@/constants' +import { BASE_URL } from '@/constants/general.consts' export default function robots(): MetadataRoute.Robots { return { diff --git a/src/app/send/[...username]/page.tsx b/src/app/send/[...username]/page.tsx index ebb8fbcf8..04db24b18 100644 --- a/src/app/send/[...username]/page.tsx +++ b/src/app/send/[...username]/page.tsx @@ -1,6 +1,7 @@ -import PaymentPage from '@/app/[...recipient]/client' import { generateMetadata as generateBaseMetadata } from '@/app/metadata' import PageContainer from '@/components/0_Bruddle/PageContainer' +import { ValidatedUsernameWrapper } from '@/components/Username/ValidatedUsernameWrapper' +import { DirectSendPageWrapper } from '@/features/payments/flows/direct-send/DirectSendPageWrapper' import { type Metadata } from 'next' import { use } from 'react' @@ -11,12 +12,13 @@ type PageProps = { export default function DirectPaymentPage(props: PageProps) { const params = use(props.params) const usernameSegments = params.username ?? [] - - const recipient = usernameSegments + const username = usernameSegments[0] ? decodeURIComponent(usernameSegments[0]) : '' return ( - + + + ) } diff --git a/src/assets/exchanges/index.ts b/src/assets/exchanges/index.ts index 66a9fb97b..c0705fca6 100644 --- a/src/assets/exchanges/index.ts +++ b/src/assets/exchanges/index.ts @@ -1,4 +1 @@ export { default as BINANCE_LOGO } from './binance.svg' -export { default as LEMON_LOGO } from './lemon.svg' -export { default as RIPIO_LOGO } from './ripio.svg' - diff --git a/src/assets/exchanges/lemon.svg b/src/assets/exchanges/lemon.svg deleted file mode 100644 index 677201915..000000000 --- a/src/assets/exchanges/lemon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/exchanges/ripio.svg b/src/assets/exchanges/ripio.svg deleted file mode 100644 index a172f071b..000000000 --- a/src/assets/exchanges/ripio.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/wallets/coinbase.svg b/src/assets/wallets/coinbase.svg deleted file mode 100644 index 37d22befe..000000000 --- a/src/assets/wallets/coinbase.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/assets/wallets/index.ts b/src/assets/wallets/index.ts index f31d91c8e..c50847abf 100644 --- a/src/assets/wallets/index.ts +++ b/src/assets/wallets/index.ts @@ -1,5 +1,2 @@ -export { default as COINBASE_LOGO } from './coinbase.svg' export { default as METAMASK_LOGO } from './metamask.svg' -export { default as RAINBOW_LOGO } from './rainbow.svg' -export { default as TRUST_WALLET_LOGO } from './trust_wallet.svg' export { default as TRUST_WALLET_SMALL_LOGO } from './trust_wallet_2.svg' diff --git a/src/assets/wallets/rainbow.svg b/src/assets/wallets/rainbow.svg deleted file mode 100644 index 7fce68b8d..000000000 --- a/src/assets/wallets/rainbow.svg +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/assets/wallets/trust_wallet.svg b/src/assets/wallets/trust_wallet.svg deleted file mode 100644 index ee816590c..000000000 --- a/src/assets/wallets/trust_wallet.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/components/0_Bruddle/Button.tsx b/src/components/0_Bruddle/Button.tsx index d73c434a0..a6604a5a5 100644 --- a/src/components/0_Bruddle/Button.tsx +++ b/src/components/0_Bruddle/Button.tsx @@ -27,7 +27,7 @@ export interface ButtonProps extends React.ButtonHTMLAttributes( if (!icon || loading) return null return (
- + {typeof icon === 'string' ? ( + + ) : ( + icon + )}
) } diff --git a/src/components/0_Bruddle/Checkbox.tsx b/src/components/0_Bruddle/Checkbox.tsx index ca00cbccf..0f991cf3a 100644 --- a/src/components/0_Bruddle/Checkbox.tsx +++ b/src/components/0_Bruddle/Checkbox.tsx @@ -1,4 +1,4 @@ -import Icon from '@/components/Global/Icon' +import { Icon } from '../Global/Icons/Icon' type CheckboxProps = { className?: string @@ -20,7 +20,7 @@ const Checkbox = ({ className, label, value, onChange }: CheckboxProps) => ( - + {label && {label}} diff --git a/src/components/0_Bruddle/RollingNumber.tsx b/src/components/0_Bruddle/RollingNumber.tsx deleted file mode 100644 index 2382cd241..000000000 --- a/src/components/0_Bruddle/RollingNumber.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect, useState } from 'react' -import { twMerge } from 'tailwind-merge' - -interface RollingNumberProps { - number: number - total: number - duration?: number - className?: string -} - -const RollingNumber = ({ number, total, duration = 2000, className }: RollingNumberProps) => { - const [displayNumber, setDisplayNumber] = useState(0) - - useEffect(() => { - let startTime: number | null = null - let animationFrame: number - - const easeOutQuart = (x: number): number => { - return 1 - Math.pow(1 - x, 4) - } - - const animate = (timestamp: number) => { - if (!startTime) startTime = timestamp - const progress = timestamp - startTime - const percentage = Math.min(progress / duration, 1) - - const easedProgress = easeOutQuart(percentage) - setDisplayNumber(Math.round(number * easedProgress)) - - if (progress < duration) { - animationFrame = requestAnimationFrame(animate) - } else { - setDisplayNumber(number) - } - } - - animationFrame = requestAnimationFrame(animate) - - return () => { - if (animationFrame) { - cancelAnimationFrame(animationFrame) - } - } - }, [number, duration]) - - return ( - - {displayNumber}/{total} - - ) -} - -export default RollingNumber diff --git a/src/components/0_Bruddle/index.ts b/src/components/0_Bruddle/index.ts deleted file mode 100644 index f8d58e765..000000000 --- a/src/components/0_Bruddle/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Card' -export * from './Button' -export * from './BaseSelect' diff --git a/src/components/ActionListCard/index.tsx b/src/components/ActionListCard/index.tsx index 1f2799d55..e6144ebbc 100644 --- a/src/components/ActionListCard/index.tsx +++ b/src/components/ActionListCard/index.tsx @@ -4,7 +4,7 @@ import Card, { type CardPosition } from '@/components/Global/Card' import { Icon } from '@/components/Global/Icons/Icon' import React from 'react' import { twMerge } from 'tailwind-merge' -import { Button } from '../0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { useHaptic } from 'use-haptic' interface ActionListCardProps { diff --git a/src/components/AddMoney/components/AddMoneyBankDetails.tsx b/src/components/AddMoney/components/AddMoneyBankDetails.tsx index 6872cca59..d95417f0a 100644 --- a/src/components/AddMoney/components/AddMoneyBankDetails.tsx +++ b/src/components/AddMoney/components/AddMoneyBankDetails.tsx @@ -12,11 +12,10 @@ import { formatCurrencyAmount } from '@/utils/currency' import { formatBankAccountDisplay } from '@/utils/format.utils' import { getCurrencyConfig, getCurrencySymbol } from '@/utils/bridge.utils' import { RequestFulfillmentBankFlowStep, useRequestFulfillmentFlow } from '@/context/RequestFulfillmentFlowContext' -import { usePaymentStore } from '@/redux/hooks' -import { formatAmount } from '@/utils' +import { formatAmount } from '@/utils/general.utils' import InfoCard from '@/components/Global/InfoCard' import CopyToClipboard from '@/components/Global/CopyToClipboard' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { useExchangeRate } from '@/hooks/useExchangeRate' interface IAddMoneyBankDetails { @@ -33,7 +32,6 @@ export default function AddMoneyBankDetails({ flow = 'add-money' }: IAddMoneyBan onrampData: requestFulfilmentOnrampData, selectedCountry: requestFulfilmentSelectedCountry, } = useRequestFulfillmentFlow() - const { chargeDetails } = usePaymentStore() // routing and country context const router = useRouter() @@ -77,7 +75,7 @@ export default function AddMoneyBankDetails({ flow = 'add-money' }: IAddMoneyBan // data from contexts based on flow const amount = isAddMoneyFlow ? onrampContext.amountToOnramp - : (requestFulfilmentOnrampData?.depositInstructions?.amount ?? chargeDetails?.tokenAmount) + : requestFulfilmentOnrampData?.depositInstructions?.amount const onrampData = isAddMoneyFlow ? onrampContext.onrampData : requestFulfilmentOnrampData const currencySymbolBasedOnCountry = useMemo(() => { @@ -187,7 +185,7 @@ ${routingLabel}: ${routingValue}` } bankDetails += ` -Deposit Reference: ${onrampData?.depositInstructions?.depositMessage || 'Loading...'} +Deposit Reference: ${onrampData?.depositInstructions?.depositMessage?.slice(0, 10) || 'Loading...'} Please use these details to complete your bank transfer.` @@ -215,7 +213,7 @@ Please use these details to complete your bank transfer.`

Amount to send

{formattedCurrencyAmount}

- +
@@ -225,13 +223,13 @@ Please use these details to complete your bank transfer.`

Deposit reference

- {onrampData?.depositInstructions?.depositMessage || 'Loading...'} + {onrampData?.depositInstructions?.depositMessage?.slice(0, 10) || 'Loading...'}

{onrampData?.depositInstructions?.depositMessage && ( )}
@@ -247,6 +245,15 @@ Please use these details to complete your bank transfer.`

Bank Details

+ {onrampData?.depositInstructions?.accountHolderName && ( + + )} + )} - {onrampData?.depositInstructions?.accountHolderName && ( - - )} - {onrampData?.depositInstructions?.bankBeneficiaryAddress && ( diff --git a/src/components/AddMoney/components/ChainChip.tsx b/src/components/AddMoney/components/ChainChip.tsx new file mode 100644 index 000000000..d3e99e2d2 --- /dev/null +++ b/src/components/AddMoney/components/ChainChip.tsx @@ -0,0 +1,22 @@ +import { Card } from '@/components/0_Bruddle/Card' +import { Icon, type IconName } from '@/components/Global/Icons/Icon' +import Image from 'next/image' + +interface ChainChipProps { + chainName: string + chainSymbol?: string + logo?: IconName + logoClassName?: string +} + +const ChainChip = ({ chainName, chainSymbol, logo, logoClassName }: ChainChipProps) => { + return ( + + {chainSymbol && {chainName}} + {logo && } +

{chainName}

+
+ ) +} + +export default ChainChip diff --git a/src/components/AddMoney/components/CryptoMethodDrawer.tsx b/src/components/AddMoney/components/CryptoMethodDrawer.tsx deleted file mode 100644 index 276a4f9c0..000000000 --- a/src/components/AddMoney/components/CryptoMethodDrawer.tsx +++ /dev/null @@ -1,100 +0,0 @@ -'use client' - -import { ARBITRUM_ICON, OTHER_CHAINS_ICON } from '@/assets' -import { Card } from '@/components/0_Bruddle' -import { Drawer, DrawerContent, DrawerTitle } from '@/components/Global/Drawer' -import Image from 'next/image' -import { useRouter } from 'next/navigation' -import React, { type Dispatch, type SetStateAction, useState } from 'react' -import TokenAndNetworkConfirmationModal from '@/components/Global/TokenAndNetworkConfirmationModal' - -const CryptoMethodDrawer = ({ - isDrawerOpen, - setisDrawerOpen, - closeDrawer, -}: { - isDrawerOpen: boolean - setisDrawerOpen: Dispatch> - closeDrawer: () => void -}) => { - const router = useRouter() - const [showRiskModal, setShowRiskModal] = useState(false) - - return ( - <> - - - Select a deposit method -
-

Select a deposit method

- - { - setisDrawerOpen(false) - setShowRiskModal(true) - }} - className={'cursor-pointer px-4 py-2 hover:bg-gray-50'} - > -
-
-
-
-
USDC on Arbitrum
- - FREE - -
- -
Recommended option for deposits
-
-
-
- Arbitrum - USDC -
-
-
- - router.push('/add-money/crypto/direct')} - className={'cursor-pointer px-4 py-2 hover:bg-gray-50'} - > -
-
-
-
Other Tokens
- -
Deposit with any token you hold
-
-
- Arbitrum -
-
-
-
-
- { - setShowRiskModal(false) - setisDrawerOpen(true) - }} - onAccept={() => { - router.push('/add-money/crypto') - }} - isVisible={showRiskModal} - /> - - ) -} - -export default CryptoMethodDrawer diff --git a/src/components/AddMoney/components/CryptoSourceListCard.tsx b/src/components/AddMoney/components/CryptoSourceListCard.tsx deleted file mode 100644 index ca1ce90f4..000000000 --- a/src/components/AddMoney/components/CryptoSourceListCard.tsx +++ /dev/null @@ -1,61 +0,0 @@ -'use client' -import AvatarWithBadge from '@/components/Profile/AvatarWithBadge' -import Image, { type StaticImageData } from 'next/image' -import { twMerge } from 'tailwind-merge' -import { type CryptoSource } from '../consts' -import { ActionListCard } from '@/components/ActionListCard' - -interface CryptoSourceListCardProps { - sources: CryptoSource[] - onItemClick: (source: CryptoSource) => void -} - -const GenericIcon = ({ type }: { type: 'exchange' | 'wallet' }) => ( - -) - -export const CryptoSourceListCard = ({ sources, onItemClick }: CryptoSourceListCardProps) => { - return ( -
- {sources.map((source, index) => ( - - ) : source.isGeneric ? ( - - ) : ( - - ) - } - onClick={() => onItemClick(source)} - position={ - sources.length === 1 - ? 'single' - : index === 0 - ? 'first' - : index === sources.length - 1 - ? 'last' - : 'middle' - } - /> - ))} -
- ) -} diff --git a/src/components/AddMoney/components/InputAmountStep.tsx b/src/components/AddMoney/components/InputAmountStep.tsx index 6ac941f8a..f91c2a20d 100644 --- a/src/components/AddMoney/components/InputAmountStep.tsx +++ b/src/components/AddMoney/components/InputAmountStep.tsx @@ -1,9 +1,9 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { Icon } from '@/components/Global/Icons/Icon' import NavHeader from '@/components/Global/NavHeader' -import TokenAmountInput from '@/components/Global/TokenAmountInput' +import AmountInput from '@/components/Global/AmountInput' import { useRouter } from 'next/navigation' import ErrorAlert from '@/components/Global/ErrorAlert' import { useCurrency } from '@/hooks/useCurrency' @@ -15,10 +15,10 @@ interface InputAmountStepProps { isLoading: boolean tokenAmount: string setTokenAmount: React.Dispatch> - setUsdAmount: React.Dispatch> error: string | null setCurrencyAmount: (amount: string | undefined) => void currencyData?: ICurrency + setCurrentDenomination?: (denomination: string) => void } const InputAmountStep = ({ @@ -27,9 +27,9 @@ const InputAmountStep = ({ onSubmit, isLoading, error, - setUsdAmount, currencyData, setCurrencyAmount, + setCurrentDenomination, }: InputAmountStepProps) => { const router = useRouter() @@ -43,20 +43,21 @@ const InputAmountStep = ({
How much do you want to add?
- setTokenAmount(e ?? '')} - setUsdValue={setUsdAmount} - currency={ +
@@ -67,7 +68,7 @@ const InputAmountStep = ({ variant="purple" shadowSize="4" onClick={onSubmit} - disabled={!!error || isLoading || !parseFloat(tokenAmount.replace(/,/g, ''))} + disabled={!!error || isLoading || !parseFloat(tokenAmount)} className="w-full" loading={isLoading} > diff --git a/src/components/AddMoney/components/MantecaAddMoney.tsx b/src/components/AddMoney/components/MantecaAddMoney.tsx index 2b2f1bc60..b80e0db24 100644 --- a/src/components/AddMoney/components/MantecaAddMoney.tsx +++ b/src/components/AddMoney/components/MantecaAddMoney.tsx @@ -1,8 +1,8 @@ 'use client' -import { type FC, useEffect, useMemo, useState } from 'react' +import { type FC, useEffect, useMemo, useState, useCallback } from 'react' import MantecaDepositShareDetails from '@/components/AddMoney/components/MantecaDepositShareDetails' import InputAmountStep from '@/components/AddMoney/components/InputAmountStep' -import { useParams, useRouter, useSearchParams } from 'next/navigation' +import { useParams, useRouter } from 'next/navigation' import { type CountryData, countryData } from '@/components/AddMoney/consts' import { type MantecaDepositResponseData } from '@/types/manteca.types' import { MantecaGeoSpecificKycModal } from '@/components/Kyc/InitiateMantecaKYCModal' @@ -11,13 +11,12 @@ import { useCurrency } from '@/hooks/useCurrency' import { useAuth } from '@/context/authContext' import { useWebSocket } from '@/hooks/useWebSocket' import { mantecaApi } from '@/services/manteca' -import { PEANUT_WALLET_TOKEN_DECIMALS, TRANSACTIONS } from '@/constants' import { parseUnits } from 'viem' import { useQueryClient } from '@tanstack/react-query' import useKycStatus from '@/hooks/useKycStatus' -import { usePaymentStore } from '@/redux/hooks' -import { saveDevConnectIntent } from '@/utils' import { MAX_MANTECA_DEPOSIT_AMOUNT, MIN_MANTECA_DEPOSIT_AMOUNT } from '@/constants/payment.consts' +import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' +import { TRANSACTIONS } from '@/constants/query.consts' interface MantecaAddMoneyProps { source: 'bank' | 'regionalMethod' @@ -28,17 +27,15 @@ type stepType = 'inputAmount' | 'depositDetails' const MantecaAddMoney: FC = ({ source }) => { const params = useParams() const router = useRouter() - const searchParams = useSearchParams() const [step, setStep] = useState('inputAmount') const [isCreatingDeposit, setIsCreatingDeposit] = useState(false) - const [tokenAmount, setTokenAmount] = useState('') const [currencyAmount, setCurrencyAmount] = useState() + const [currentDenomination, setCurrentDenomination] = useState('USD') const [usdAmount, setUsdAmount] = useState('') const [error, setError] = useState(null) const [depositDetails, setDepositDetails] = useState() const [isKycModalOpen, setIsKycModalOpen] = useState(false) const queryClient = useQueryClient() - const { parsedPaymentData } = usePaymentStore() const selectedCountryPath = params.country as string const selectedCountry = useMemo(() => { @@ -66,7 +63,7 @@ const MantecaAddMoney: FC = ({ source }) => { setError(null) return } - const paymentAmount = parseUnits(usdAmount.replace(/,/g, ''), PEANUT_WALLET_TOKEN_DECIMALS) + const paymentAmount = parseUnits(usdAmount, PEANUT_WALLET_TOKEN_DECIMALS) if (paymentAmount < parseUnits(MIN_MANTECA_DEPOSIT_AMOUNT.toString(), PEANUT_WALLET_TOKEN_DECIMALS)) { setError(`Deposit amount must be at least $${MIN_MANTECA_DEPOSIT_AMOUNT}`) } else if (paymentAmount > parseUnits(MAX_MANTECA_DEPOSIT_AMOUNT.toString(), PEANUT_WALLET_TOKEN_DECIMALS)) { @@ -89,7 +86,7 @@ const MantecaAddMoney: FC = ({ source }) => { } } - const handleAmountSubmit = async () => { + const handleAmountSubmit = useCallback(async () => { if (!selectedCountry?.currency) return if (isCreatingDeposit) return @@ -107,8 +104,11 @@ const MantecaAddMoney: FC = ({ source }) => { try { setError(null) setIsCreatingDeposit(true) + const isUsdDenominated = currentDenomination === 'USD' + const amount = isUsdDenominated ? usdAmount : currencyAmount const depositData = await mantecaApi.deposit({ - usdAmount: usdAmount.replace(/,/g, ''), + amount: amount!, + isUsdDenominated, currency: selectedCountry.currency, }) if (depositData.error) { @@ -116,10 +116,6 @@ const MantecaAddMoney: FC = ({ source }) => { return } setDepositDetails(depositData.data) - - // @dev: save devconnect intent if this is a devconnect flow - to be deleted post devconnect - saveDevConnectIntent(user?.user?.userId, parsedPaymentData, usdAmount, depositData.data?.externalId) - setStep('depositDetails') } catch (error) { console.log(error) @@ -127,7 +123,7 @@ const MantecaAddMoney: FC = ({ source }) => { } finally { setIsCreatingDeposit(false) } - } + }, [currentDenomination, selectedCountry, usdAmount, currencyAmount]) // handle verification modal opening useEffect(() => { @@ -142,14 +138,14 @@ const MantecaAddMoney: FC = ({ source }) => { return ( <> {isKycModalOpen && ( x.alpha2 as string) ) -const enabledBankWithdrawCountries = new Set( - [...Object.values(BRIDGE_ALPHA3_TO_ALPHA2), 'US', 'MX', 'AR'].filter((code) => !NON_EUR_SEPA_ALPHA2.has(code)) -) +const enabledBankWithdrawCountries = new Set([...Object.values(BRIDGE_ALPHA3_TO_ALPHA2), 'US', 'MX', 'AR']) // exclude non-euro sepa countries from bank deposits, same as withdrawals -const enabledBankDepositCountries = new Set( - [...Object.values(BRIDGE_ALPHA3_TO_ALPHA2), 'US', 'MX', 'AR'].filter((code) => !NON_EUR_SEPA_ALPHA2.has(code)) -) +const enabledBankDepositCountries = new Set([...Object.values(BRIDGE_ALPHA3_TO_ALPHA2), 'US', 'MX', 'AR']) // Helper function to check if a country code is enabled for bank transfers // Handles both 2-letter and 3-letter country codes diff --git a/src/components/AddMoney/views/CryptoDepositQR.view.tsx b/src/components/AddMoney/views/CryptoDepositQR.view.tsx index 4b039b224..ec733a3fb 100644 --- a/src/components/AddMoney/views/CryptoDepositQR.view.tsx +++ b/src/components/AddMoney/views/CryptoDepositQR.view.tsx @@ -5,6 +5,7 @@ import CopyToClipboard from '@/components/Global/CopyToClipboard' import NavHeader from '@/components/Global/NavHeader' import QRCodeWrapper from '@/components/Global/QRCodeWrapper' import AvatarWithBadge from '@/components/Profile/AvatarWithBadge' +import { useAutoTruncatedAddress } from '@/hooks/useAutoTruncatedAddress' import Image, { type StaticImageData } from 'next/image' import { useRouter } from 'next/navigation' @@ -26,6 +27,7 @@ export const CryptoDepositQR = ({ chainIcon, }: CryptoDepositQRProps) => { const router = useRouter() + const { containerRef, truncatedAddress } = useAutoTruncatedAddress(depositAddress) return (
@@ -57,12 +59,16 @@ export const CryptoDepositQR = ({ - -

- {depositAddress} + +

+ {truncatedAddress}

- - +
- } - /> - ) - })} -
-
-
- ) -} - -export default NetworkSelectionView diff --git a/src/components/AddMoney/views/RhinoDeposit.view.tsx b/src/components/AddMoney/views/RhinoDeposit.view.tsx new file mode 100644 index 000000000..6ccc328b7 --- /dev/null +++ b/src/components/AddMoney/views/RhinoDeposit.view.tsx @@ -0,0 +1,284 @@ +'use client' +import { Button } from '@/components/0_Bruddle/Button' +import Card from '@/components/Global/Card' +import CopyToClipboard, { type CopyToClipboardRef } from '@/components/Global/CopyToClipboard' +import { Icon } from '@/components/Global/Icons/Icon' +import NavHeader from '@/components/Global/NavHeader' +import QRCodeWrapper from '@/components/Global/QRCodeWrapper' +import ChainChip from '../components/ChainChip' +import InfoCard from '@/components/Global/InfoCard' +import { Root, List, Trigger } from '@radix-ui/react-tabs' +import PeanutLoading from '@/components/Global/PeanutLoading' +import { useQuery } from '@tanstack/react-query' +import { rhinoApi } from '@/services/rhino' +import { useState, useEffect, useMemo, useRef } from 'react' +import type { CreateDepositAddressResponse, RhinoChainType } from '@/services/services.types' +import { useAutoTruncatedAddress } from '@/hooks/useAutoTruncatedAddress' +import { CHAIN_LOGOS, RHINO_SUPPORTED_TOKENS, SUPPORTED_EVM_CHAINS } from '@/constants/rhino.consts' +import UserCard from '@/components/User/UserCard' +import { isCryptoAddress, printableAddress } from '@/utils/general.utils' + +interface RhinoDepositViewProps { + onBack?: () => void + chainType: RhinoChainType + setChainType: (chainType: RhinoChainType) => void + depositAddressData: CreateDepositAddressResponse | undefined + isDepositAddressDataLoading: boolean + headerTitle: string + onSuccess: (amount: number) => void + showUserCard?: boolean + amount?: number + identifier?: string +} + +const RhinoDepositView = ({ + onBack, + chainType, + setChainType, + depositAddressData, + isDepositAddressDataLoading, + headerTitle, + onSuccess, + showUserCard = false, + amount, + identifier, +}: RhinoDepositViewProps) => { + const [isDelayComplete, setIsDelayComplete] = useState(false) + const [isUpdatingDepositAddresStatus, setisUpdatingDepositAddresStatus] = useState(false) + const copyRef = useRef(null) + + const POLLING_DELAY = 15_000 + + useEffect(() => { + const timer = setTimeout(() => setIsDelayComplete(true), POLLING_DELAY) + return () => clearTimeout(timer) + }, []) + + const { data: depositAddressStatusData } = useQuery({ + queryKey: ['rhino-deposit-address-status', depositAddressData?.depositAddress], + queryFn: () => { + if (!depositAddressData?.depositAddress) { + throw new Error('Deposit address is required') + } + return rhinoApi.getDepositAddressStatus(depositAddressData.depositAddress as string) + }, + enabled: !!depositAddressData?.depositAddress && isDelayComplete, // Add some delay to start polling after the deposit address is created + refetchInterval: (query) => (query.state.data?.status === 'completed' ? false : 5000), + }) + + const { containerRef, truncatedAddress } = useAutoTruncatedAddress(depositAddressData?.depositAddress ?? '') + + const depositAddressStatus = useMemo(() => { + if (depositAddressStatusData?.status === 'accepted') { + return 'loading' + } else if (depositAddressStatusData?.status === 'pending') { + return 'loading' + } else if (depositAddressStatusData?.status === 'failed') { + return 'failed' + } else if (depositAddressStatusData?.status === 'completed') { + return 'completed' + } else { + return 'not_started' + } + }, [depositAddressStatusData]) + + // Optimistic update of the deposit address status + const updateDepositAddressStatus = async () => { + if (isUpdatingDepositAddresStatus) return // Prevent concurrent calls + + if (!depositAddressData?.depositAddress) { + return + } + + setisUpdatingDepositAddresStatus(true) + await rhinoApi.resetDepositAddressStatus(depositAddressData.depositAddress) + setisUpdatingDepositAddresStatus(false) + } + + const amountLimitsTitle = useMemo(() => { + if (chainType === 'EVM') { + return 'EVM networks' + } else if (chainType === 'SOL') { + return 'Solana' + } else if (chainType === 'TRON') { + return 'Tron' + } + }, [chainType]) + + useEffect(() => { + if (depositAddressStatus === 'completed' && depositAddressStatusData?.amount) { + onSuccess(depositAddressStatusData?.amount) + } + }, [depositAddressStatusData, depositAddressStatus, onSuccess]) + + if (depositAddressStatus === 'failed') { + return ( +
+ + +
+ +
+
+ +
+

Oops! Market moved

+ +

+ The exchange rate changed too much to complete your deposit. +

+ +

+ Your money is on its way back to your wallet. +

+
+
+ +
+
+ ) + } + + return ( +
+ + +
+ {showUserCard && ( + + )} + setChainType(e as RhinoChainType)} + defaultValue="EVM" + className="w-full" + > + + + EVM + + + Solana + + + Tron + + + + + {(isDepositAddressDataLoading || depositAddressStatus === 'loading') && ( +
+ +
+ )} + + {depositAddressData && !isDepositAddressDataLoading && ( + <> +
+ +
+ + + + +

Supported tokens:

+ {RHINO_SUPPORTED_TOKENS.filter((token) => { + if (chainType === 'TRON') { + return token.name !== 'USDC' + } + return true + }).map((token) => ( + + ))} +
+ } + /> + +
+
+
+ +

Min deposit for {amountLimitsTitle}

+
+ +

+ {depositAddressData.minDepositLimitUsd} USD +

+
+ +
+
+ +

Max deposit for {amountLimitsTitle}

+
+ +

+ {depositAddressData.maxDepositLimitUsd} USD +

+
+
+ + {chainType === 'EVM' && ( + +

Supported EVM networks

+
+ {SUPPORTED_EVM_CHAINS.map((chain) => ( + + ))} +
+
+ )} + + )} +
+
+ ) +} + +export default RhinoDepositView diff --git a/src/components/AddMoney/views/TokenSelection.view.tsx b/src/components/AddMoney/views/TokenSelection.view.tsx index ad3500178..1e61104c4 100644 --- a/src/components/AddMoney/views/TokenSelection.view.tsx +++ b/src/components/AddMoney/views/TokenSelection.view.tsx @@ -1,11 +1,11 @@ import StatusBadge from '@/components/Global/Badges/StatusBadge' import { getCardPosition } from '@/components/Global/Card' import NavHeader from '@/components/Global/NavHeader' -import { PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants' import Image from 'next/image' import React from 'react' import { type CryptoToken, DEPOSIT_CRYPTO_TOKENS } from '../consts' import { ActionListCard } from '@/components/ActionListCard' +import { PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants/zerodev.consts' interface TokenSelectionViewProps { headerTitle?: string diff --git a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx index 428705b27..c45b02d52 100644 --- a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx +++ b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx @@ -12,7 +12,7 @@ import EmptyState from '../Global/EmptyStates/EmptyState' import { useAuth } from '@/context/authContext' import { useEffect, useMemo, useRef, useState } from 'react' import { DynamicBankAccountForm, type IBankAccountDetails } from './DynamicBankAccountForm' -import { addBankAccount, updateUserById } from '@/app/actions/users' +import { addBankAccount } from '@/app/actions/users' import { type BridgeKycStatus } from '@/utils/bridge-accounts.utils' import { type AddBankAccountPayload } from '@/app/actions/types/users.types' import { useWebSocket } from '@/hooks/useWebSocket' @@ -20,13 +20,13 @@ import { useWithdrawFlow } from '@/context/WithdrawFlowContext' import { type Account } from '@/interfaces' import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils' import { DeviceType, useDeviceType } from '@/hooks/useGetDeviceType' -import CryptoMethodDrawer from '../AddMoney/components/CryptoMethodDrawer' import { useAppDispatch } from '@/redux/hooks' import { bankFormActions } from '@/redux/slices/bank-form-slice' import { InitiateBridgeKYCModal } from '../Kyc/InitiateBridgeKYCModal' import useKycStatus from '@/hooks/useKycStatus' import KycVerifiedOrReviewModal from '../Global/KycVerifiedOrReviewModal' import { ActionListCard } from '@/components/ActionListCard' +import TokenAndNetworkConfirmationModal from '../Global/TokenAndNetworkConfirmationModal' interface AddWithdrawCountriesListProps { flow: 'add' | 'withdraw' @@ -55,7 +55,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { const [liveKycStatus, setLiveKycStatus] = useState( user?.user?.bridgeKycStatus as BridgeKycStatus ) - const [isDrawerOpen, setIsDrawerOpen] = useState(false) + const [isSupportedTokensModalOpen, setIsSupportedTokensModalOpen] = useState(false) const { isUserBridgeKycUnderReview } = useKycStatus() const [showKycStatusModal, setShowKycStatusModal] = useState(false) @@ -89,12 +89,11 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { const currentKycStatus = liveKycStatus || user?.user.bridgeKycStatus const isUserKycVerified = currentKycStatus === 'approved' - const hasNameOnLoad = !!user?.user.fullName const hasEmailOnLoad = !!user?.user.email - // scenario (1): happy path: if the user has already completed kyc and we have their name and email, - // we can add the bank account directly. - if (isUserKycVerified && (hasNameOnLoad || rawData.firstName) && (hasEmailOnLoad || rawData.email)) { + // scenario (1): happy path: if the user has already completed kyc, we can add the bank account directly + // note: we no longer check for fullName as account owner name is now always collected from the form + if (isUserKycVerified && (hasEmailOnLoad || rawData.email)) { const currentAccountIds = new Set(user?.accounts.map((acc) => acc.id) ?? []) const result = await addBankAccount(payload) @@ -117,13 +116,13 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { // fallback to the previous method if we can't find the new account // this can happen if the user object is not updated immediately const newAccountFromResponse = result.data as Account - if (!newAccountFromResponse.details) { - newAccountFromResponse.details = { - countryCode: payload.countryCode, - countryName: payload.countryName, - bankName: null, - accountOwnerName: `${payload.accountOwnerName.firstName} ${payload.accountOwnerName.lastName}`, - } + // ensure details has accountOwnerName for confirmation page display + newAccountFromResponse.details = { + ...(newAccountFromResponse.details || {}), + countryCode: payload.countryCode, + countryName: payload.countryName, + bankName: newAccountFromResponse.details?.bankName || null, + accountOwnerName: `${payload.accountOwnerName.firstName} ${payload.accountOwnerName.lastName}`, } setSelectedBankAccount(newAccountFromResponse) } @@ -135,22 +134,6 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { return {} } - // if the user's profile is missing their full name or email, - // we update it with the data they just provided in the form. - if (!hasNameOnLoad || !hasEmailOnLoad) { - if (user?.user.userId && rawData.firstName && rawData.lastName && rawData.email) { - const result = await updateUserById({ - userId: user.user.userId, - fullName: `${rawData.firstName} ${rawData.lastName}`.trim(), - email: rawData.email, - }) - if (result.error) { - return { error: result.error } - } - await fetchUser() // refetch user data to get updated name/email - } - } - // scenario (2): if the user hasn't completed kyc yet if (!isUserKycVerified) { setIsKycModalOpen(true) @@ -209,7 +192,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { const handleAddMethodClick = (method: SpecificPaymentMethod) => { if (method.path) { if (method.id === 'crypto-add') { - setIsDrawerOpen(true) + setIsSupportedTokensModalOpen(true) return } // show kyc status modal if user is kyc under review @@ -402,10 +385,14 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { renderPaymentMethods('Choose withdrawing method', methods.withdraw)}
{flow === 'add' && ( - setIsDrawerOpen(false)} + { + setIsSupportedTokensModalOpen(false) + }} + onAccept={() => { + router.push('/add-money/crypto') + }} + isVisible={isSupportedTokensModalOpen} /> )} = ({ const [savedAccounts, setSavedAccounts] = useState([]) // local flag only for add flow; for withdraw we derive from context const [localShowAllMethods, setLocalShowAllMethods] = useState(false) - const [isDrawerOpen, setIsDrawerOpen] = useState(false) + const [isSupportedTokensModalOpen, setIsSupportedTokensModalOpen] = useState(false) const [, startTransition] = useTransition() const searchParams = useSearchParams() const currencyCode = searchParams.get('currencyCode') @@ -129,7 +134,7 @@ export const AddWithdrawRouterView: FC = ({ } if (flow === 'add' && method.id === 'crypto') { - setIsDrawerOpen(true) + setIsSupportedTokensModalOpen(true) return } @@ -158,6 +163,9 @@ export const AddWithdrawRouterView: FC = ({ const defaultBackNavigation = () => router.push('/home') + // check if we're coming from request fulfillment or similar flow + const fromRequestFulfillment = typeof window !== 'undefined' && getFromLocalStorage('fromRequestFulfillment') + if (isLoadingPreferences) { return (
@@ -208,7 +216,7 @@ export const AddWithdrawRouterView: FC = ({ // preserve method param if coming from send flow const additionalParams = isBankFromSend ? `&method=${methodParam}` : '' router.push( - `/withdraw/manteca?country=${account.details.countryName}&destination=${account.identifier}${additionalParams}` + `/withdraw/manteca?country=${account.details.countryName}&destination=${account.identifier}&isSavedAccount=true${additionalParams}` ) } }} @@ -246,10 +254,14 @@ export const AddWithdrawRouterView: FC = ({ - setIsDrawerOpen(false)} + { + setIsSupportedTokensModalOpen(false) + }} + onAccept={() => { + router.push('/add-money/crypto') + }} + isVisible={isSupportedTokensModalOpen} />
) @@ -261,7 +273,18 @@ export const AddWithdrawRouterView: FC = ({ { - if (shouldShowAllMethods) { + // if coming from request fulfillment or similar external flow, go back immediately + if (fromRequestFulfillment) { + if (onBackClick) { + onBackClick() + } else { + defaultBackNavigation() + } + return + } + + // otherwise, use toggle logic for better ux when user manually navigated to "select new method" + if (shouldShowAllMethods && (recentMethodsState.length > 0 || savedAccounts.length > 0)) { setShouldShowAllMethods(false) } else if (onBackClick) { onBackClick() @@ -274,10 +297,17 @@ export const AddWithdrawRouterView: FC = ({ { // from send flow (bank): set method in context and stay on /withdraw?method=bank if (flow === 'withdraw' && isBankFromSend) { + if (isMantecaCountry(country.path)) { + const route = `/withdraw/manteca?method=bank-transfer&country=${country.path}` + startTransition(() => { + router.push(route) + }) + return + } + // set selected method and let withdraw page move to amount input setSelectedMethod({ type: 'bridge', @@ -302,7 +332,7 @@ export const AddWithdrawRouterView: FC = ({ }} onCryptoClick={() => { if (flow === 'add') { - setIsDrawerOpen(true) + setIsSupportedTokensModalOpen(true) } else { // preserve method param if coming from send flow (though crypto shouldn't show this screen) const queryParams = methodParam ? `?method=${methodParam}` : '' @@ -320,10 +350,15 @@ export const AddWithdrawRouterView: FC = ({ }} flow={flow} /> - setIsDrawerOpen(false)} + + { + setIsSupportedTokensModalOpen(false) + }} + onAccept={() => { + router.push('/add-money/crypto') + }} + isVisible={isSupportedTokensModalOpen} />
) diff --git a/src/components/AddWithdraw/DynamicBankAccountForm.tsx b/src/components/AddWithdraw/DynamicBankAccountForm.tsx index f13c3eea0..a2ffab55d 100644 --- a/src/components/AddWithdraw/DynamicBankAccountForm.tsx +++ b/src/components/AddWithdraw/DynamicBankAccountForm.tsx @@ -12,16 +12,15 @@ import { validateIban, validateBic, isValidRoutingNumber } from '@/utils/bridge- import ErrorAlert from '@/components/Global/ErrorAlert' import { getBicFromIban } from '@/app/actions/ibanToBic' import PeanutActionDetailsCard, { type PeanutActionDetailsCardProps } from '../Global/PeanutActionDetailsCard' -import { PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants' import { useWithdrawFlow } from '@/context/WithdrawFlowContext' import { getCountryFromIban, validateMXCLabeAccount, validateUSBankAccount } from '@/utils/withdraw.utils' import useSavedAccounts from '@/hooks/useSavedAccounts' import { useAppDispatch, useAppSelector } from '@/redux/hooks' import { bankFormActions } from '@/redux/slices/bank-form-slice' import { useDebounce } from '@/hooks/useDebounce' -import { Icon } from '../Global/Icons/Icon' import { twMerge } from 'tailwind-merge' import { MX_STATES, US_STATES } from '@/constants/stateCodes.consts' +import { PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants/zerodev.consts' const isIBANCountry = (country: string) => { return BRIDGE_ALPHA3_TO_ALPHA2[country.toUpperCase()] !== undefined @@ -31,6 +30,7 @@ export type IBankAccountDetails = { name?: string firstName: string lastName: string + accountOwnerName?: string // single field for withdraw flow email: string accountNumber: string bic: string @@ -78,8 +78,6 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D const [submissionError, setSubmissionError] = useState(null) const { country: countryNameParams } = useParams() const { amountToWithdraw, setSelectedBankAccount } = useWithdrawFlow() - const [firstName, ...lastNameParts] = (user?.user.fullName ?? '').split(' ') - const lastName = lastNameParts.join(' ') const router = useRouter() const savedAccounts = useSavedAccounts() const [isCheckingBICValid, setisCheckingBICValid] = useState(false) @@ -90,6 +88,9 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D // Get persisted form data from Redux const persistedFormData = useAppSelector((state) => state.bankForm.formData) + // for claim flow: pre-fill accountOwnerName from user if logged in, for withdraw flow: keep empty + const defaultAccountOwnerName = flow === 'claim' && user?.user.fullName ? user.user.fullName : '' + const { control, handleSubmit, @@ -99,9 +100,10 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D formState: { errors, isValid, isValidating, touchedFields }, } = useForm({ defaultValues: { - firstName: firstName ?? '', - lastName: lastName ?? '', - email: user?.user.email ?? '', + firstName: '', // kept for backwards compatibility but not used in UI + lastName: '', // kept for backwards compatibility but not used in UI + accountOwnerName: defaultAccountOwnerName, + email: flow === 'claim' ? (user?.user.email ?? '') : '', // only pre-fill email in claim flow accountNumber: '', bic: '', routingNumber: '', @@ -170,7 +172,24 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D const accountNumber = isMx ? data.clabe : data.accountNumber - const { firstName, lastName } = data + // split accountOwnerName into first and last name for all flows + // note: bridge api requires both first_name and last_name for individual accounts, + // so we validate that accountOwnerName contains at least 2 words in the form + let firstName: string + let lastName: string + + if (data.accountOwnerName) { + // split the trimmed name into parts using one or more whitespace characters as the separator + // this allows to handle cases where the name has multiple parts like "Peanut Guy" or "Happy Peanut Guy" + const nameParts = data.accountOwnerName.trim().split(/\s+/) + firstName = nameParts[0] || '' + lastName = nameParts.slice(1).join(' ') || '' + } else { + // fallback to firstName/lastName if accountOwnerName is not set (backwards compatibility) + firstName = data.firstName || '' + lastName = data.lastName || '' + } + let bic = data.bic || getValues('bic') const iban = data.iban || getValues('iban') @@ -204,8 +223,8 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D accountNumber: isIban ? '' : data.accountNumber, bic: bic, country, - firstName: data.firstName.trim(), - lastName: data.lastName.trim(), + firstName: firstName.trim(), + lastName: lastName.trim(), name: data.name, }) if (result.error) { @@ -216,8 +235,8 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D const formDataToSave = { ...data, country, - firstName: data.firstName.trim(), - lastName: data.lastName.trim(), + firstName: firstName.trim(), + lastName: lastName.trim(), } dispatch(bankFormActions.setFormData(formDataToSave)) setIsSubmitting(false) @@ -335,16 +354,35 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D }} className="space-y-4" > + {/* CLAIM FLOW: show name field for guest users or logged-in users without fullName */} {flow === 'claim' && !user?.user.userId && (
- {renderInput('firstName', 'First Name', { required: 'First name is required' })} - {renderInput('lastName', 'Last Name', { required: 'Last name is required' })} + {renderInput('accountOwnerName', 'Account Owner Name', { + required: 'Account owner name is required', + validate: (value: string) => { + const trimmed = value.trim() + const parts = trimmed.split(/\s+/) + if (parts.length < 2) { + return 'Please enter both first and last name' + } + return true + }, + })}
)} {flow === 'claim' && user?.user.userId && !user.user.fullName && (
- {renderInput('firstName', 'First Name', { required: 'First name is required' })} - {renderInput('lastName', 'Last Name', { required: 'Last name is required' })} + {renderInput('accountOwnerName', 'Account Owner Name', { + required: 'Account owner name is required', + validate: (value: string) => { + const trimmed = value.trim() + const parts = trimmed.split(/\s+/) + if (parts.length < 2) { + return 'Please enter both first and last name' + } + return true + }, + })}
)} {flow === 'claim' && @@ -354,10 +392,21 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D renderInput('email', 'E-mail', { required: 'Email is required', })} - {flow !== 'claim' && !user?.user?.fullName && ( + + {/* WITHDRAW FLOW: always show account owner's name field (empty by default) */} + {flow !== 'claim' && (
- {renderInput('firstName', 'First Name', { required: 'First name is required' })} - {renderInput('lastName', 'Last Name', { required: 'Last name is required' })} + {renderInput('accountOwnerName', 'Account Owner Name', { + required: 'Account owner name is required', + validate: (value: string) => { + const trimmed = value.trim() + const parts = trimmed.split(/\s+/) + if (parts.length < 2) { + return 'Please enter both first and last name' + } + return true + }, + })}
)} {flow !== 'claim' && diff --git a/src/components/Badges/BadgeStatusDrawer.tsx b/src/components/Badges/BadgeStatusDrawer.tsx index 0511e5ce1..1204a57ab 100644 --- a/src/components/Badges/BadgeStatusDrawer.tsx +++ b/src/components/Badges/BadgeStatusDrawer.tsx @@ -1,10 +1,12 @@ import { Drawer, DrawerContent } from '@/components/Global/Drawer' import Image from 'next/image' -import { formatDate } from '@/utils' +import { formatDate } from '@/utils/general.utils' import Card from '../Global/Card' import { PaymentInfoRow } from '../Payment/PaymentInfoRow' import ShareButton from '../Global/ShareButton' import { getBadgeIcon } from './badge.utils' +import { BASE_URL } from '@/constants/general.consts' +import { useAuth } from '@/context/authContext' export type BadgeStatusDrawerProps = { isOpen: boolean @@ -20,9 +22,14 @@ export type BadgeStatusDrawerProps = { // shows a drawer for a newly unlocked badge export const BadgeStatusDrawer = ({ isOpen, onClose, badge }: BadgeStatusDrawerProps) => { + const { user: authUser } = useAuth() + const username = authUser?.user.username const earnedAt = badge.earnedAt ? new Date(badge.earnedAt) : undefined const dateStr = earnedAt ? formatDate(earnedAt) : undefined + // generate profile link for sharing + const profileLink = username ? `${BASE_URL}/${username}` : BASE_URL + return ( @@ -58,7 +65,7 @@ export const BadgeStatusDrawer = ({ isOpen, onClose, badge }: BadgeStatusDrawerP title="" generateText={() => Promise.resolve( - `${badge.description}\nI just unlocked a ${badge.name} badge on Peanut!` + `I earned ${badge.name} badge on Peanut!\n\nJoin Peanut now and start earning points, unlocking achievements and moving money worldwide\n\n${profileLink}` ) } > diff --git a/src/components/Badges/BadgesRow.tsx b/src/components/Badges/BadgesRow.tsx index 91701128f..bda161c90 100644 --- a/src/components/Badges/BadgesRow.tsx +++ b/src/components/Badges/BadgesRow.tsx @@ -5,7 +5,7 @@ import Image from 'next/image' import { useEffect, useMemo, useRef, useState, useCallback } from 'react' import { Tooltip } from '../Tooltip' import { twMerge } from 'tailwind-merge' -import { Button } from '../0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { Icon } from '../Global/Icons/Icon' import { getBadgeIcon, getPublicBadgeDescription } from './badge.utils' @@ -31,7 +31,7 @@ interface BadgesRowProps { * - Tooltips showing badge name and description on hover * - Automatic sorting by earned date (newest first) */ -export function BadgesRow({ badges, className, isSelfProfile = true }: BadgesRowProps) { +const BadgesRow = ({ badges, className, isSelfProfile = true }: BadgesRowProps) => { const viewportRef = useRef(null) const [visibleCount, setVisibleCount] = useState(4) const [startIdx, setStartIdx] = useState(0) diff --git a/src/components/Claim/Claim.consts.ts b/src/components/Claim/Claim.consts.ts index 877d28683..e33d64289 100644 --- a/src/components/Claim/Claim.consts.ts +++ b/src/components/Claim/Claim.consts.ts @@ -1,8 +1,8 @@ -import * as consts from '@/constants' import * as interfaces from '@/interfaces' import { type IOfframpSuccessScreenProps, type IOfframpConfirmScreenProps } from '../Offramp/Offramp.consts' import { type ClaimLinkData } from '@/services/sendLinks' import { type PeanutCrossChainRoute } from '@/services/swap' +import type { IOfframpForm } from '@/constants/cashout.consts' export type ClaimType = 'claim' | 'claimxchain' export type ClaimScreens = 'INITIAL' | 'CONFIRM' | 'SUCCESS' @@ -48,8 +48,8 @@ export interface IClaimScreenProps { setHasFetchedRoute: (fetched: boolean) => void recipientType: interfaces.RecipientType setRecipientType: (type: interfaces.RecipientType) => void - offrampForm: consts.IOfframpForm - setOfframpForm: (form: consts.IOfframpForm) => void + offrampForm: IOfframpForm + setOfframpForm: (form: IOfframpForm) => void isOfframpPossible: boolean userType: 'NEW' | 'EXISTING' | undefined setUserType: (type: 'NEW' | 'EXISTING' | undefined) => void diff --git a/src/components/Claim/Claim.tsx b/src/components/Claim/Claim.tsx index c2a41ced0..36bc6fbc2 100644 --- a/src/components/Claim/Claim.tsx +++ b/src/components/Claim/Claim.tsx @@ -1,13 +1,11 @@ 'use client' import { generateKeysFromString } from '@squirrel-labs/peanut-sdk' -import { useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { useContext, useEffect, useMemo, useState } from 'react' import { fetchTokenDetails, fetchTokenPrice } from '@/app/actions/tokens' -import { Button } from '@/components/0_Bruddle' import { type StatusType } from '@/components/Global/Badges/StatusBadge' import { TransactionDetailsReceipt } from '@/components/TransactionDetails/TransactionDetailsReceipt' import { type TransactionDetails, REWARD_TOKENS } from '@/components/TransactionDetails/transactionTransformer' -import * as consts from '@/constants' import { tokenSelectorContext } from '@/context' import { useAuth } from '@/context/authContext' import { useTransactionDetailsDrawer } from '@/hooks/useTransactionDetailsDrawer' @@ -16,7 +14,14 @@ import { useUserInteractions } from '@/hooks/useUserInteractions' import { useWallet } from '@/hooks/wallet/useWallet' import * as interfaces from '@/interfaces' import { ESendLinkStatus, getParamsFromLink, sendLinksApi, type ClaimLinkData } from '@/services/sendLinks' -import { getInitialsFromName, getTokenDetails, isStableCoin } from '@/utils' +import { + getInitialsFromName, + getTokenDetails, + isStableCoin, + getChainName, + getTokenLogo, + getChainLogo, +} from '@/utils/general.utils' import * as Sentry from '@sentry/nextjs' import { useQuery } from '@tanstack/react-query' import type { Hash } from 'viem' @@ -31,6 +36,7 @@ import { twMerge } from 'tailwind-merge' import { ClaimBankFlowStep, useClaimBankFlow } from '@/context/ClaimBankFlowContext' import { useSearchParams } from 'next/navigation' import { useHaptic } from 'use-haptic' +import type { IOfframpForm } from '@/constants/cashout.consts' export const Claim = ({}) => { const [linkUrl, setLinkUrl] = useState('') @@ -53,7 +59,7 @@ export const Claim = ({}) => { const [hasFetchedRoute, setHasFetchedRoute] = useState(false) const [recipientType, setRecipientType] = useState('address') - const [offrampForm, setOfframpForm] = useState({ + const [offrampForm, setOfframpForm] = useState({ name: '', email: '', password: '', @@ -130,26 +136,56 @@ export const Claim = ({}) => { const rewardData = REWARD_TOKENS[claimLinkData.tokenAddress.toLowerCase()] + // determine direction based on user role and status + let direction: TransactionDetails['direction'] = 'send' + if (status === 'completed') { + // if link is claimed, show as send from sender's perspective + direction = 'send' + } + + // determine recipient name + const recipientName = + claimLinkData.claim?.recipient?.username ?? claimLinkData.claim?.recipientAddress ?? 'Send via Link' + + // find the claimed event for timestamp + const claimedEvent = claimLinkData.events?.find((e) => e.status === 'CLAIMED') + let details: Partial = { id: claimLinkData.pubKey, + direction, status, amount: Number(formatUnits(claimLinkData.amount, tokenDetails?.decimals ?? 6)), date: new Date(claimLinkData.createdAt), + createdAt: new Date(claimLinkData.createdAt), + claimedAt: claimedEvent ? new Date(claimedEvent.timestamp) : undefined, tokenSymbol: tokenDetails?.symbol, - initials: getInitialsFromName(claimLinkData.claim?.recipient?.username ?? ''), + tokenAddress: claimLinkData.tokenAddress, + initials: getInitialsFromName(recipientName), memo: claimLinkData.textContent, attachmentUrl: claimLinkData.fileUrl, - cancelledDate: status === 'cancelled' ? new Date(claimLinkData.events[0].timestamp) : undefined, + cancelledDate: status === 'cancelled' ? new Date(claimLinkData.events[0]?.timestamp) : undefined, + txHash: claimLinkData.claim?.txHash, extraDataForDrawer: { isLinkTransaction: true, originalType: EHistoryEntryType.SEND_LINK, originalUserRole: EHistoryUserRole.SENDER, link: claimLinkData.link, rewardData, + transactionCardType: 'send', }, - userName: - claimLinkData.claim?.recipient?.username ?? claimLinkData.claim?.recipientAddress ?? 'Send via Link', + userName: recipientName, + fullName: claimLinkData.claim?.recipient?.username ?? recipientName, sourceView: 'history', + tokenDisplayDetails: tokenDetails + ? { + tokenSymbol: tokenDetails.symbol, + chainName: getChainName(claimLinkData.chainId), + tokenIconUrl: getTokenLogo(tokenDetails.symbol), + chainIconUrl: getChainName(claimLinkData.chainId) + ? getChainLogo(getChainName(claimLinkData.chainId)!) + : undefined, + } + : undefined, peanutFeeDetails: { amountDisplay: '$ 0.00', }, diff --git a/src/components/Claim/Generic/ClaimError.view.tsx b/src/components/Claim/Generic/ClaimError.view.tsx index 10a142266..c1f878231 100644 --- a/src/components/Claim/Generic/ClaimError.view.tsx +++ b/src/components/Claim/Generic/ClaimError.view.tsx @@ -1,7 +1,7 @@ 'use client' -import { Button } from '@/components/0_Bruddle' -import { useSupportModalContext } from '@/context/SupportModalContext' +import { Button } from '@/components/0_Bruddle/Button' +import { useModalsContext } from '@/context/ModalsContext' import Image from 'next/image' import Link from 'next/link' import PEANUTMAN_CRY from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_05.gif' @@ -14,7 +14,7 @@ type ClaimErrorViewProps = { } export const ClaimErrorView = ({ title, message, primaryButtonText, onPrimaryClick }: ClaimErrorViewProps) => { - const { openSupportWithMessage } = useSupportModalContext() + const { openSupportWithMessage } = useModalsContext() return (
diff --git a/src/components/Claim/Generic/Claimed.view.tsx b/src/components/Claim/Generic/Claimed.view.tsx index 8330b05fe..5e5accfab 100644 --- a/src/components/Claim/Generic/Claimed.view.tsx +++ b/src/components/Claim/Generic/Claimed.view.tsx @@ -1,9 +1,10 @@ 'use client' -import { Button, Card } from '@/components/0_Bruddle' import { Icon } from '@/components/Global/Icons/Icon' import { useAuth } from '@/context/authContext' import { useRouter } from 'next/navigation' import { type FC } from 'react' +import { Button } from '@/components/0_Bruddle/Button' +import { Card } from '@/components/0_Bruddle/Card' interface ClaimedViewProps { amount: number | bigint diff --git a/src/components/Claim/Link/FlowManager.tsx b/src/components/Claim/Link/FlowManager.tsx index 6dcec133d..66e0c32bf 100644 --- a/src/components/Claim/Link/FlowManager.tsx +++ b/src/components/Claim/Link/FlowManager.tsx @@ -22,7 +22,6 @@ const FlowManager = ({ }) => { const viewComponents: _consts.IFlowManagerClaimComponents = { INITIAL: InitialClaimLinkView, - // todo: @dev note, handle bank claims in links-v2 project CONFIRM: onchainViews.ConfirmClaimLinkView, SUCCESS: onchainViews.SuccessClaimLinkView, } diff --git a/src/components/Claim/Link/Initial.view.tsx b/src/components/Claim/Link/Initial.view.tsx index 324ffb6da..e203a72e7 100644 --- a/src/components/Claim/Link/Initial.view.tsx +++ b/src/components/Claim/Link/Initial.view.tsx @@ -10,21 +10,15 @@ import { optimismChainId, usdcAddressOptimism, } from '@/components/Offramp/Offramp.consts' -import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN, ROUTE_NOT_FOUND_ERROR } from '@/constants' import { TRANSACTIONS } from '@/constants/query.consts' import { loadingStateContext, tokenSelectorContext } from '@/context' import { useAuth } from '@/context/authContext' import { useWallet } from '@/hooks/wallet/useWallet' import { sendLinksApi } from '@/services/sendLinks' -import { - areEvmAddressesEqual, - ErrorHandler, - fetchWithSentry, - formatTokenAmount, - getBridgeChainName, - getBridgeTokenName, - printableAddress, -} from '@/utils' +import { areEvmAddressesEqual, formatTokenAmount, printableAddress } from '@/utils/general.utils' +import { ErrorHandler } from '@/utils/sdkErrorHandler.utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { getBridgeChainName, getBridgeTokenName } from '@/utils/bridge-accounts.utils' import { NATIVE_TOKEN_ADDRESS, SQUID_ETH_ADDRESS, checkTokenSupportsXChain } from '@/utils/token.utils' import * as Sentry from '@sentry/nextjs' import { useQueryClient } from '@tanstack/react-query' @@ -33,14 +27,14 @@ import { useCallback, useContext, useEffect, useMemo, useState, useRef } from 'r import { formatUnits, isAddress, zeroAddress } from 'viem' import type { Address } from 'viem' import { type IClaimScreenProps } from '../Claim.consts' -import ActionList from '@/components/Common/ActionList' +import SendLinkActionList from '@/components/Claim/Link/SendLinkActionList' import { ClaimBankFlowStep, useClaimBankFlow } from '@/context/ClaimBankFlowContext' import useClaimLink from '../useClaimLink' import ActionModal from '@/components/Global/ActionModal' import { Slider } from '@/components/Slider' import { BankFlowManager } from './views/BankFlowManager.view' import { type PeanutCrossChainRoute, getRoute } from '@/services/swap' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import Image from 'next/image' import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets' import { GuestVerificationModal } from '@/components/Global/GuestVerificationModal' @@ -49,6 +43,8 @@ import MantecaFlowManager from './MantecaFlowManager' import ErrorAlert from '@/components/Global/ErrorAlert' import { invitesApi } from '@/services/invites' import { EInviteType } from '@/services/services.types' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants/zerodev.consts' +import { ROUTE_NOT_FOUND_ERROR } from '@/constants/general.consts' export const InitialClaimLinkView = (props: IClaimScreenProps) => { // get campaign tag from claim link url @@ -62,7 +58,6 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { recipient, tokenPrice, setClaimType, - setEstimatedPoints, attachment, setTransactionHash, onCustom, @@ -976,8 +971,7 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { )} {!claimToExternalWallet && ( - void +} + +/** + * shows available payment methods for claiming a send link + * note: request flow uses RequestPotActionList instead + */ +export default function SendLinkActionList({ + claimLinkData, + isLoggedIn, + isInviteLink = false, + showDevconnectMethod, + setExternalWalletRecipient, +}: ISendLinkActionListProps) { + const router = useRouter() + const { + setClaimToExternalWallet, + setFlowStep: setClaimBankFlowStep, + setShowVerificationModal, + setClaimToMercadoPago, + setRegionalMethodType, + setHideTokenSelector, + } = useClaimBankFlow() + const [showMinAmountError, setShowMinAmountError] = useState(false) + const { claimType } = useDetermineBankClaimType(claimLinkData?.sender?.userId ?? '') + const savedAccounts = useSavedAccounts() + const { addParamStep } = useClaimLink() + const [selectedMethod, setSelectedMethod] = useState(null) + const [showInviteModal, setShowInviteModal] = useState(false) + const { user } = useAuth() + const { + setSelectedTokenAddress, + setSelectedChainID, + devconnectChainId, + devconnectRecipientAddress, + devconnectTokenAddress, + } = useContext(tokenSelectorContext) + const { isUserMantecaKycApproved } = useKycStatus() + const dispatch = useAppDispatch() + + const requiresVerification = useMemo(() => { + return claimType === BankClaimType.GuestKycNeeded || claimType === BankClaimType.ReceiverKycNeeded + }, [claimType]) + + // filter and sort payment methods based on geolocation + const { filteredMethods: sortedActionMethods, isLoading: isGeoLoading } = useGeoFilteredPaymentOptions({ + sortUnavailable: true, + isMethodUnavailable: (method) => + method.soon || + (method.id === 'bank' && requiresVerification) || + (['mercadopago', 'pix'].includes(method.id) && !isUserMantecaKycApproved), + methods: showDevconnectMethod + ? DEVCONNECT_CLAIM_METHODS.filter((method) => method.id !== 'devconnect') + : undefined, + }) + + const handleMethodClick = async (method: PaymentMethod) => { + const amountInUsd = parseFloat(formatUnits(claimLinkData.amount, claimLinkData.tokenDecimals)) + if (method.id === 'bank' && !validateMinimumAmount(amountInUsd, method.id)) { + setShowMinAmountError(true) + return + } + + switch (method.id) { + case 'bank': + if (claimType === BankClaimType.GuestKycNeeded) { + addParamStep('bank') + setShowVerificationModal(true) + } else { + if (savedAccounts.length) { + setClaimBankFlowStep(ClaimBankFlowStep.SavedAccountsList) + } else { + setClaimBankFlowStep(ClaimBankFlowStep.BankCountryList) + } + } + break + case 'mercadopago': + case 'pix': + if (!user) { + addParamStep('regional-claim') + setShowVerificationModal(true) + return + } + setRegionalMethodType(method.id) + setClaimToMercadoPago(true) + break + case 'devconnect': + setExternalWalletRecipient?.({ + address: devconnectRecipientAddress, + name: undefined, + }) + setSelectedTokenAddress(devconnectTokenAddress) + setSelectedChainID(devconnectChainId) + setHideTokenSelector(true) + setClaimToExternalWallet(true) + break + case 'exchange-or-wallet': + setClaimToExternalWallet(true) + break + } + } + + const handleContinueWithPeanut = () => { + addParamStep('claim') + const redirectUri = encodeURIComponent(window.location.pathname + window.location.search + window.location.hash) + const rawUsername = claimLinkData?.sender?.username + if (isInviteLink && !userHasAppAccess && rawUsername) { + const username = rawUsername.toUpperCase() + const inviteCode = `${username}INVITESYOU` + dispatch(setupActions.setInviteCode(inviteCode)) + dispatch(setupActions.setInviteType(EInviteType.PAYMENT_LINK)) + router.push(`/invite?code=${inviteCode}&redirect_uri=${redirectUri}`) + } else { + router.push(`/setup?redirect_uri=${redirectUri}`) + } + } + + const username = claimLinkData?.sender?.username + const userHasAppAccess = user?.user?.hasAppAccess ?? false + const devconnectMethod = DEVCONNECT_CLAIM_METHODS.find((m) => m.id === 'devconnect')! + + if (isGeoLoading) { + return ( +
+ +
+ ) + } + + return ( +
+ {showDevconnectMethod && ( + <> + + + + )} + + {!isLoggedIn && ( + + )} + + {SHOW_INVITE_MODAL_FOR_DEVCONNECT && isInviteLink && !userHasAppAccess && username && ( +
+ star +

Invited by {username}, you have early access!

+ star +
+ )} + + + +
+ {sortedActionMethods.map((method) => { + let methodRequiresVerification = method.id === 'bank' && requiresVerification + if (!isUserMantecaKycApproved && ['mercadopago', 'pix'].includes(method.id)) { + methodRequiresVerification = true + } + + return ( + { + if (isInviteLink && !userHasAppAccess && method.id !== 'devconnect') { + setSelectedMethod(method) + setShowInviteModal(true) + } else { + handleMethodClick(method) + } + }} + key={method.id} + method={method} + requiresVerification={methodRequiresVerification} + /> + ) + })} +
+ + {!isLoggedIn && } + + setShowMinAmountError(false)} + title="Minimum Amount" + description={`The minimum amount for this payment method is $${MIN_BANK_TRANSFER_AMOUNT}. Please enter a higher amount or try a different method.`} + icon="alert" + ctas={[{ text: 'Close', shadowSize: '4', onClick: () => setShowMinAmountError(false) }]} + iconContainerClassName="bg-yellow-400" + preventClose={false} + modalPanelClassName="max-w-md mx-8" + /> + + { + if (selectedMethod) { + handleMethodClick(selectedMethod) + setShowInviteModal(false) + setSelectedMethod(null) + } + }} + isOpen={showInviteModal} + onClose={() => { + setShowInviteModal(false) + setSelectedMethod(null) + }} + /> +
+ ) +} + +const MethodCard = ({ + method, + onClick, + requiresVerification, + isDisabled, +}: { + method: PaymentMethod + onClick: () => void + requiresVerification?: boolean + isDisabled?: boolean +}) => { + return ( + + {method.title} + {(method.soon || requiresVerification) && ( + + )} +
+ } + onClick={onClick} + isDisabled={method.soon || isDisabled} + rightContent={} + /> + ) +} diff --git a/src/components/Claim/Link/views/BankFlowManager.view.tsx b/src/components/Claim/Link/views/BankFlowManager.view.tsx index 9be300927..f19e1ccd5 100644 --- a/src/components/Claim/Link/views/BankFlowManager.view.tsx +++ b/src/components/Claim/Link/views/BankFlowManager.view.tsx @@ -8,7 +8,8 @@ import { loadingStateContext } from '@/context' import { createBridgeExternalAccountForGuest } from '@/app/actions/external-accounts' import { confirmOfframp, createOfframp, createOfframpForGuest } from '@/app/actions/offramp' import { type Address, formatUnits } from 'viem' -import { ErrorHandler, formatTokenAmount } from '@/utils' +import { ErrorHandler } from '@/utils/sdkErrorHandler.utils' +import { formatTokenAmount } from '@/utils/general.utils' import * as Sentry from '@sentry/nextjs' import useClaimLink from '../../useClaimLink' import { type AddBankAccountPayload } from '@/app/actions/types/users.types' @@ -30,7 +31,6 @@ import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils' import { useAppDispatch } from '@/redux/hooks' import { bankFormActions } from '@/redux/slices/bank-form-slice' import { sendLinksApi } from '@/services/sendLinks' -import { useNonEurSepaRedirect } from '@/hooks/useNonEurSepaRedirect' import { InitiateBridgeKYCModal } from '@/components/Kyc/InitiateBridgeKYCModal' import { useSearchParams } from 'next/navigation' @@ -87,12 +87,6 @@ export const BankFlowManager = (props: IClaimScreenProps) => { const [isProcessingKycSuccess, setIsProcessingKycSuccess] = useState(false) const [offrampData, setOfframpData] = useState(null) - // redirect back to country list if selected country is a non-eur sepa country (blocked) - const { isBlocked: isCountryBlocked } = useNonEurSepaRedirect({ - countryIdentifier: selectedCountry?.id || selectedCountry?.path, - shouldRedirect: false, // we handle redirect manually in useEffect - }) - // websocket for real-time KYC status updates useWebSocket({ username: user?.user.username ?? undefined, @@ -109,17 +103,6 @@ export const BankFlowManager = (props: IClaimScreenProps) => { } }, [user?.user.bridgeKycStatus]) - // redirect back to country list if trying to access a blocked non-eur sepa country - useEffect(() => { - if ( - isCountryBlocked && - (claimBankFlowStep === ClaimBankFlowStep.BankDetailsForm || - claimBankFlowStep === ClaimBankFlowStep.BankConfirmClaim) - ) { - setClaimBankFlowStep(ClaimBankFlowStep.BankCountryList) - } - }, [isCountryBlocked, claimBankFlowStep, setClaimBankFlowStep]) - /** * @name handleConfirmClaim * @description claims the link to the deposit address provided by the off-ramp api and confirms the transfer. @@ -428,11 +411,9 @@ export const BankFlowManager = (props: IClaimScreenProps) => { onPrev={() => setClaimBankFlowStep(null)} savedAccounts={savedAccounts} onAccountClick={async (account) => { - const [firstName, ...lastNameParts] = ( - account.details.accountOwnerName || - user?.user.fullName || - '' - ).split(' ') + // for saved accounts, use the user's full name (these are assumed to be user's own accounts) + const fullNameToUse = user?.user.fullName || '' + const [firstName, ...lastNameParts] = fullNameToUse.split(' ') const lastName = lastNameParts.join(' ') const bankDetails = { @@ -473,13 +454,7 @@ export const BankFlowManager = (props: IClaimScreenProps) => { /> ) case ClaimBankFlowStep.BankCountryList: - return ( - - ) + return case ClaimBankFlowStep.BankDetailsForm: return (
diff --git a/src/components/Claim/Link/views/Confirm.bank-claim.view.tsx b/src/components/Claim/Link/views/Confirm.bank-claim.view.tsx index e14d12e99..c3372bc0d 100644 --- a/src/components/Claim/Link/views/Confirm.bank-claim.view.tsx +++ b/src/components/Claim/Link/views/Confirm.bank-claim.view.tsx @@ -1,6 +1,6 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { ALL_COUNTRIES_ALPHA3_TO_ALPHA2 } from '@/components/AddMoney/consts' import Card from '@/components/Global/Card' import ErrorAlert from '@/components/Global/ErrorAlert' diff --git a/src/components/Claim/Link/views/MantecaDetailsStep.view.tsx b/src/components/Claim/Link/views/MantecaDetailsStep.view.tsx index 988053c44..163715577 100644 --- a/src/components/Claim/Link/views/MantecaDetailsStep.view.tsx +++ b/src/components/Claim/Link/views/MantecaDetailsStep.view.tsx @@ -1,13 +1,13 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { Icon } from '@/components/Global/Icons/Icon' import { MercadoPagoStep } from '@/types/manteca.types' import { type Dispatch, type FC, type SetStateAction, useState } from 'react' -import { MANTECA_COUNTRIES_CONFIG } from '@/constants' import ValidatedInput from '@/components/Global/ValidatedInput' import { validateCbuCvuAlias } from '@/utils/withdraw.utils' import ErrorAlert from '@/components/Global/ErrorAlert' +import { MANTECA_COUNTRIES_CONFIG } from '@/constants/manteca.consts' interface MantecaDetailsStepProps { setCurrentStep: Dispatch> diff --git a/src/components/Claim/Link/views/MantecaReviewStep.tsx b/src/components/Claim/Link/views/MantecaReviewStep.tsx index ec7e6ea72..7d88100fa 100644 --- a/src/components/Claim/Link/views/MantecaReviewStep.tsx +++ b/src/components/Claim/Link/views/MantecaReviewStep.tsx @@ -1,8 +1,7 @@ -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import ErrorAlert from '@/components/Global/ErrorAlert' import MantecaDetailsCard, { type MantecaCardRow } from '@/components/Global/MantecaDetailsCard' import PeanutLoading from '@/components/Global/PeanutLoading' -import { MANTECA_DEPOSIT_ADDRESS } from '@/constants' import { useCurrency } from '@/hooks/useCurrency' import { mantecaApi } from '@/services/manteca' import { sendLinksApi } from '@/services/sendLinks' @@ -10,6 +9,7 @@ import { MercadoPagoStep } from '@/types/manteca.types' import { type Dispatch, type FC, type SetStateAction, useState } from 'react' import useClaimLink from '@/components/Claim/useClaimLink' import * as Sentry from '@sentry/nextjs' +import { MANTECA_DEPOSIT_ADDRESS } from '@/constants/manteca.consts' interface MantecaReviewStepProps { setCurrentStep: Dispatch> @@ -100,7 +100,7 @@ const MantecaReviewStep: FC = ({ } const { data, error: withdrawError } = await mantecaApi.withdraw({ - amount: amount.replace(/,/g, ''), + amount, destinationAddress, txHash, currency, diff --git a/src/components/Claim/useClaimLink.tsx b/src/components/Claim/useClaimLink.tsx index 5e60aeb86..9a73f1223 100644 --- a/src/components/Claim/useClaimLink.tsx +++ b/src/components/Claim/useClaimLink.tsx @@ -14,11 +14,11 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { usePathname, useSearchParams } from 'next/navigation' import { captureException } from '@sentry/nextjs' -import { next_proxy_url } from '@/constants' import { CLAIM_LINK, CLAIM_LINK_XCHAIN, TRANSACTIONS } from '@/constants/query.consts' import { loadingStateContext } from '@/context' -import { isTestnetChain } from '@/utils' +import { isTestnetChain } from '@/utils/general.utils' import { sendLinksApi, ESendLinkStatus } from '@/services/sendLinks' +import { next_proxy_url } from '@/constants/general.consts' // ============================================================================ // Constants diff --git a/src/components/Common/ActionList.tsx b/src/components/Common/ActionList.tsx deleted file mode 100644 index c1de9f64a..000000000 --- a/src/components/Common/ActionList.tsx +++ /dev/null @@ -1,498 +0,0 @@ -'use client' - -import StatusBadge from '../Global/Badges/StatusBadge' -import IconStack from '../Global/IconStack' -import { ClaimBankFlowStep, useClaimBankFlow } from '@/context/ClaimBankFlowContext' -import { type ClaimLinkData } from '@/services/sendLinks' -import { formatUnits } from 'viem' -import { useContext, useMemo, useState, useRef } from 'react' -import ActionModal from '@/components/Global/ActionModal' -import Divider from '../0_Bruddle/Divider' -import { Button } from '../0_Bruddle' -import { PEANUT_LOGO_BLACK } from '@/assets/illustrations' -import Image from 'next/image' -import { useRouter } from 'next/navigation' -import { PEANUTMAN_LOGO } from '@/assets/peanut' -import { BankClaimType, useDetermineBankClaimType } from '@/hooks/useDetermineBankClaimType' -import useSavedAccounts from '@/hooks/useSavedAccounts' -import { useRequestFulfillmentFlow } from '@/context/RequestFulfillmentFlowContext' -import { type ParsedURL } from '@/lib/url-parser/types/payment' -import { useAppDispatch, usePaymentStore } from '@/redux/hooks' -import { BankRequestType, useDetermineBankRequestType } from '@/hooks/useDetermineBankRequestType' -import { GuestVerificationModal } from '../Global/GuestVerificationModal' -import ActionListDaimoPayButton from './ActionListDaimoPayButton' -import { DEVCONNECT_CLAIM_METHODS, type PaymentMethod } from '@/constants/actionlist.consts' -import useClaimLink from '../Claim/useClaimLink' -import { setupActions } from '@/redux/slices/setup-slice' -import starStraightImage from '@/assets/icons/starStraight.svg' -import { useAuth } from '@/context/authContext' -import { EInviteType } from '@/services/services.types' -import ConfirmInviteModal from '../Global/ConfirmInviteModal' -import Loading from '../Global/Loading' -import { useWallet } from '@/hooks/wallet/useWallet' -import { ActionListCard } from '../ActionListCard' -import { useGeoFilteredPaymentOptions } from '@/hooks/useGeoFilteredPaymentOptions' -import { tokenSelectorContext } from '@/context' -import SupportCTA from '../Global/SupportCTA' -import { DEVCONNECT_LOGO } from '@/assets' -import useKycStatus from '@/hooks/useKycStatus' -import { usePaymentInitiator } from '@/hooks/usePaymentInitiator' -import { MIN_BANK_TRANSFER_AMOUNT, validateMinimumAmount } from '@/constants' - -const SHOW_INVITE_MODAL_FOR_DEVCONNECT = false - -interface IActionListProps { - flow: 'claim' | 'request' - claimLinkData?: ClaimLinkData - requestLinkData?: ParsedURL - isLoggedIn: boolean - isInviteLink?: boolean - showDevconnectMethod?: boolean - setExternalWalletRecipient?: (recipient: { name: string | undefined; address: string }) => void - usdAmount?: string -} - -/** - * Shows a list of available payment methods to choose from for claiming a send link or fullfilling a request link - * - * @param {object} props - * @param {ClaimLinkData} props.claimLinkData The claim link data - * @param {boolean} props.isLoggedIn Whether the user is logged in, used to show cta for continue with peanut if not logged in - * @returns {JSX.Element} - */ -export default function ActionList({ - claimLinkData, - isLoggedIn, - flow, - requestLinkData, - isInviteLink = false, - showDevconnectMethod, - setExternalWalletRecipient, - usdAmount: usdAmountValue, -}: IActionListProps) { - const router = useRouter() - const { - setClaimToExternalWallet, - setFlowStep: setClaimBankFlowStep, - setShowVerificationModal, - setClaimToMercadoPago, - setRegionalMethodType, - setHideTokenSelector, - } = useClaimBankFlow() - const { balance } = useWallet() - const [showMinAmountError, setShowMinAmountError] = useState(false) - const { claimType } = useDetermineBankClaimType(claimLinkData?.sender?.userId ?? '') - const { chargeDetails, usdAmount, parsedPaymentData } = usePaymentStore() - const requesterUserId = chargeDetails?.requestLink?.recipientAccount?.userId ?? '' - const { requestType } = useDetermineBankRequestType(requesterUserId) - const savedAccounts = useSavedAccounts() - const { addParamStep } = useClaimLink() - const { - setShowRequestFulfilmentBankFlowManager, - setFlowStep: setRequestFulfilmentBankFlowStep, - setFulfillUsingManteca, - setRegionalMethodType: setRequestFulfillmentRegionalMethodType, - setTriggerPayWithPeanut, - } = useRequestFulfillmentFlow() - const [isGuestVerificationModalOpen, setIsGuestVerificationModalOpen] = useState(false) - const [selectedMethod, setSelectedMethod] = useState(null) - const [showInviteModal, setShowInviteModal] = useState(false) - const { user } = useAuth() - const { - setSelectedTokenAddress, - setSelectedChainID, - devconnectChainId, - devconnectRecipientAddress, - devconnectTokenAddress, - } = useContext(tokenSelectorContext) - const [isUsePeanutBalanceModalShown, setIsUsePeanutBalanceModalShown] = useState(false) - const [showUsePeanutBalanceModal, setShowUsePeanutBalanceModal] = useState(false) - const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(null) - const { initiatePayment, loadingStep } = usePaymentInitiator() - const { isUserMantecaKycApproved } = useKycStatus() - const isPaymentInProgress = loadingStep !== 'Idle' && loadingStep !== 'Error' && loadingStep !== 'Success' - // ref to store daimo button click handler for triggering from balance modal - const daimoButtonClickRef = useRef<(() => void) | null>(null) - - const dispatch = useAppDispatch() - - const requiresVerification = useMemo(() => { - if (flow === 'claim') { - return claimType === BankClaimType.GuestKycNeeded || claimType === BankClaimType.ReceiverKycNeeded - } - if (flow === 'request') { - return requestType === BankRequestType.GuestKycNeeded || requestType === BankRequestType.PayerKycNeeded - } - return false - }, [claimType, requestType, flow]) - - // use the hook to filter and sort payment methods based on geolocation - const { filteredMethods: sortedActionMethods, isLoading: isGeoLoading } = useGeoFilteredPaymentOptions({ - sortUnavailable: true, - isMethodUnavailable: (method) => - method.soon || - (method.id === 'bank' && requiresVerification) || - (['mercadopago', 'pix'].includes(method.id) && !isUserMantecaKycApproved), - methods: showDevconnectMethod - ? DEVCONNECT_CLAIM_METHODS.filter((method) => method.id !== 'devconnect') - : undefined, - }) - - // Check if user has enough Peanut balance to pay for the request - const amountInUsd = usdAmount ? parseFloat(usdAmount) : 0 - const hasSufficientPeanutBalance = user && balance && Number(balance) >= amountInUsd - - // check if amount is valid for request flow - const currentRequestAmount = usdAmountValue ?? usdAmount - const requestAmountValue = currentRequestAmount ? parseFloat(currentRequestAmount) : 0 - const isAmountEntered = flow === 'request' ? !!currentRequestAmount && requestAmountValue > 0 : true - - const handleMethodClick = async (method: PaymentMethod, bypassBalanceModal = false) => { - // validate minimum amount for bank/mercado pago/pix in request flow - if (flow === 'request' && requestLinkData) { - // check minimum amount for bank/mercado pago/pix - if ( - ['bank', 'mercadopago', 'pix'].includes(method.id) && - !validateMinimumAmount(requestAmountValue, method.id) - ) { - setShowMinAmountError(true) - return - } - } - - // For request flow: Check if user has sufficient Peanut balance and hasn't dismissed the modal - if (flow === 'request' && requestLinkData && !bypassBalanceModal) { - if (!isUsePeanutBalanceModalShown && hasSufficientPeanutBalance) { - setSelectedPaymentMethod(method) // Store the method they want to use - setShowUsePeanutBalanceModal(true) - return // Show modal, don't proceed with method yet - } - } - - if (flow === 'claim' && claimLinkData) { - const amountInUsd = parseFloat(formatUnits(claimLinkData.amount, claimLinkData.tokenDecimals)) - if (method.id === 'bank' && !validateMinimumAmount(amountInUsd, method.id)) { - setShowMinAmountError(true) - return - } - switch (method.id) { - case 'bank': - { - if (claimType === BankClaimType.GuestKycNeeded) { - addParamStep('bank') - setShowVerificationModal(true) - } else { - if (savedAccounts.length) { - setClaimBankFlowStep(ClaimBankFlowStep.SavedAccountsList) - } else { - setClaimBankFlowStep(ClaimBankFlowStep.BankCountryList) - } - } - } - break - case 'mercadopago': - case 'pix': - if (!user) { - addParamStep('regional-claim') - setShowVerificationModal(true) - return - } - setRegionalMethodType(method.id) - setClaimToMercadoPago(true) - break - case 'devconnect': - setExternalWalletRecipient?.({ - address: devconnectRecipientAddress, // Address sent from devconnect app - name: undefined, - }) - // For devconnect claims we need to set address and chain from the url params - setSelectedTokenAddress(devconnectTokenAddress) - setSelectedChainID(devconnectChainId) - setHideTokenSelector(true) - setClaimToExternalWallet(true) - break - case 'exchange-or-wallet': - setClaimToExternalWallet(true) - break - } - } else if (flow === 'request' && requestLinkData) { - // for bank/mercadopago/pix in request flow, redirect to add-money flow - switch (method.id) { - case 'bank': - case 'mercadopago': - case 'pix': - if (user?.user) { - // user is logged in, redirect to add-money - router.push('/add-money') - } else { - // user is not logged in, save redirect url and go to setup - const redirectUri = encodeURIComponent('/add-money') - router.push(`/setup?redirect_uri=${redirectUri}`) - } - break - // 'exchange-or-wallet' case removed - handled by ActionListDaimoPayButton - } - } - } - - const handleContinueWithPeanut = () => { - if (flow === 'claim') { - addParamStep('claim') - } - // push to setup page with redirect uri, to prevent the user from losing the flow context - const redirectUri = encodeURIComponent(window.location.pathname + window.location.search + window.location.hash) - const rawUsername = - flow === 'request' ? requestLinkData?.recipient?.identifier : claimLinkData?.sender?.username - if (isInviteLink && !userHasAppAccess && rawUsername) { - const username = rawUsername ? rawUsername.toUpperCase() : '' - const inviteCode = `${username}INVITESYOU` - dispatch(setupActions.setInviteCode(inviteCode)) - dispatch(setupActions.setInviteType(EInviteType.PAYMENT_LINK)) - router.push(`/invite?code=${inviteCode}&redirect_uri=${redirectUri}`) - } else { - router.push(`/setup?redirect_uri=${redirectUri}`) - } - } - - const username = claimLinkData?.sender?.username ?? requestLinkData?.recipient?.identifier - const userHasAppAccess = user?.user?.hasAppAccess ?? false - - const devconnectMethod = DEVCONNECT_CLAIM_METHODS.find((m) => m.id === 'devconnect')! - - if (isGeoLoading) { - return ( -
- -
- ) - } - - return ( -
- {/* TODO @dev remove this after devconnect app testing phase */} - {showDevconnectMethod && ( - <> - - - - - )} - - {!isLoggedIn && ( - - )} - {SHOW_INVITE_MODAL_FOR_DEVCONNECT && isInviteLink && !userHasAppAccess && username && ( -
- star{' '} -

Invited by {username}, you have early access!

- star -
- )} - -
- {sortedActionMethods.map((method) => { - if (flow === 'request' && method.id === 'exchange-or-wallet') { - return ( -
- { - // Check balance before showing Daimo widget - if (!isUsePeanutBalanceModalShown && hasSufficientPeanutBalance) { - setSelectedPaymentMethod(method) - setShowUsePeanutBalanceModal(true) - return false // Don't show Daimo yet - } - return true // Proceed with Daimo - }} - isDisabled={!isAmountEntered} - clickHandlerRef={daimoButtonClickRef} - /> -
- ) - } - - let methodRequiresVerification = method.id === 'bank' && requiresVerification - - if (!isUserMantecaKycApproved && ['mercadopago', 'pix'].includes(method.id)) { - methodRequiresVerification = true - } - - return ( - { - if (isInviteLink && !userHasAppAccess && method.id !== 'devconnect') { - setSelectedMethod(method) - setShowInviteModal(true) - } else { - handleMethodClick(method) - } - }} - key={method.id} - method={method} - requiresVerification={ - methodRequiresVerification || - (['mercadopago', 'pix'].includes(method.id) && !isUserMantecaKycApproved) - } - isDisabled={!isAmountEntered} - /> - ) - })} -
- {flow === 'claim' && !isLoggedIn && } - setShowMinAmountError(false)} - title="Minimum Amount" - description={`The minimum amount for this payment method is $${MIN_BANK_TRANSFER_AMOUNT}. Please enter a higher amount or try a different method.`} - icon="alert" - ctas={[{ text: 'Close', shadowSize: '4', onClick: () => setShowMinAmountError(false) }]} - iconContainerClassName="bg-yellow-400" - preventClose={false} - modalPanelClassName="max-w-md mx-8" - /> - - setIsGuestVerificationModalOpen(false)} - description="To fulfill this request using bank account, please create an account and verify your identity." - redirectToVerification - /> - - {/* Invites modal */} - - { - if (selectedMethod) { - handleMethodClick(selectedMethod) - setShowInviteModal(false) - setSelectedMethod(null) - } - }} - isOpen={showInviteModal} - onClose={() => { - setShowInviteModal(false) - setSelectedMethod(null) - }} - /> - - { - setShowUsePeanutBalanceModal(false) - setIsUsePeanutBalanceModalShown(true) - setSelectedPaymentMethod(null) - }} - title="Use your Peanut balance instead" - description={ - 'You already have enough funds in your Peanut account. Using this method is instant and avoids delays.' - } - icon="user-plus" - ctas={[ - { - text: 'Pay with Peanut', - shadowSize: '4', - onClick: () => { - setShowUsePeanutBalanceModal(false) - setIsUsePeanutBalanceModalShown(true) - setSelectedPaymentMethod(null) - setTriggerPayWithPeanut(true) - }, - }, - { - text: 'Continue', - shadowSize: '4', - variant: 'stroke', - onClick: () => { - setShowUsePeanutBalanceModal(false) - setIsUsePeanutBalanceModalShown(true) - // Proceed with the method the user originally selected - if (selectedPaymentMethod) { - // for exchange-or-wallet, trigger daimo button after state updates - if (selectedPaymentMethod.id === 'exchange-or-wallet' && daimoButtonClickRef.current) { - // use setTimeout to ensure state updates are processed before triggering daimo - setTimeout(() => { - daimoButtonClickRef.current?.() - }, 0) - } else { - // for other methods, use handleMethodClick - handleMethodClick(selectedPaymentMethod, true) // true = bypass modal check - } - } - setSelectedPaymentMethod(null) - }, - }, - ]} - iconContainerClassName="bg-primary-1" - preventClose={false} - modalPanelClassName="max-w-md mx-8" - /> -
- ) -} - -export const MethodCard = ({ - method, - onClick, - requiresVerification, - isDisabled, -}: { - method: PaymentMethod - onClick: () => void - requiresVerification?: boolean - isDisabled?: boolean -}) => { - return ( - - {method.title} - {(method.soon || requiresVerification) && ( - - )} -
- } - onClick={onClick} - isDisabled={method.soon || isDisabled} - rightContent={} - /> - ) -} diff --git a/src/components/Common/ActionListDaimoPayButton.tsx b/src/components/Common/ActionListDaimoPayButton.tsx deleted file mode 100644 index 609978550..000000000 --- a/src/components/Common/ActionListDaimoPayButton.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { useCallback, useState, useRef } from 'react' -import IconStack from '../Global/IconStack' -import { useAppDispatch, usePaymentStore } from '@/redux/hooks' -import { paymentActions } from '@/redux/slices/payment-slice' -import { useCurrency } from '@/hooks/useCurrency' -import { useSearchParams } from 'next/navigation' -import { type InitiatePaymentPayload, usePaymentInitiator } from '@/hooks/usePaymentInitiator' -import DaimoPayButton from '../Global/DaimoPayButton' -import { ACTION_METHODS } from '@/constants/actionlist.consts' -import { useWallet } from '@/hooks/wallet/useWallet' -import ConfirmInviteModal from '../Global/ConfirmInviteModal' -import { ActionListCard } from '../ActionListCard' -import { DaimoPayWrapper } from '../Global/DaimoPayWrapper' - -interface ActionListDaimoPayButtonProps { - handleContinueWithPeanut: () => void - showConfirmModal: boolean - onBeforeShow?: () => boolean | Promise - isDisabled?: boolean - clickHandlerRef?: React.MutableRefObject<(() => void) | null> -} - -const ActionListDaimoPayButton = ({ - handleContinueWithPeanut, - showConfirmModal, - onBeforeShow, - isDisabled, - clickHandlerRef, -}: ActionListDaimoPayButtonProps) => { - const dispatch = useAppDispatch() - const searchParams = useSearchParams() - const method = ACTION_METHODS.find((method) => method.id === 'exchange-or-wallet') - const { requestDetails, chargeDetails, attachmentOptions, parsedPaymentData, usdAmount } = usePaymentStore() - const { - code: currencyCode, - symbol: currencySymbol, - price: currencyPrice, - } = useCurrency(searchParams.get('currency')) - const requestId = searchParams.get('id') - const { address: peanutWalletAddress } = useWallet() - const [showInviteModal, setShowInviteModal] = useState(false) - const [confirmLoseInvite, setConfirmLoseInvite] = useState(false) - const daimoPayButtonClickRef = useRef<(() => void) | null>(null) - - const { isProcessing, initiateDaimoPayment, completeDaimoPayment } = usePaymentInitiator() - - const handleInitiateDaimoPayment = useCallback(async () => { - if (!usdAmount || parseFloat(usdAmount) <= 0) { - console.error('Invalid amount entered') - dispatch(paymentActions.setError('Invalid amount')) - return false - } - - if (!parsedPaymentData) { - console.error('Invalid payment data') - dispatch(paymentActions.setError('Something went wrong. Please try again or contact support.')) - return false - } - - dispatch(paymentActions.setError(null)) - dispatch(paymentActions.setDaimoError(null)) - let tokenAmount = usdAmount - - const payload: InitiatePaymentPayload = { - recipient: parsedPaymentData?.recipient, - tokenAmount, - requestId: requestId ?? undefined, - chargeId: chargeDetails?.uuid, - currency: currencyCode - ? { - code: currencyCode, - symbol: currencySymbol || '', - price: currencyPrice?.buy || 0, - } - : undefined, - currencyAmount: usdAmount, - isExternalWalletFlow: false, - transactionType: 'DIRECT_SEND', - attachmentOptions: attachmentOptions, - } - - console.log('Initiating Daimo payment', payload) - - const result = await initiateDaimoPayment(payload) - - if (result.status === 'Charge Created') { - console.log('Charge created!!') - return true - } else if (result.status === 'Error') { - dispatch(paymentActions.setError('Something went wrong. Please try again or contact support.')) - console.error('Payment initiation failed:', result) - return false - } else { - console.warn('Unexpected status from usePaymentInitiator:', result.status) - dispatch(paymentActions.setError('Something went wrong. Please try again or contact support.')) - return false - } - }, [ - usdAmount, - dispatch, - chargeDetails, - requestDetails, - requestId, - parsedPaymentData, - attachmentOptions, - initiateDaimoPayment, - currencyCode, - currencySymbol, - currencyPrice, - ]) - - const handleCompleteDaimoPayment = useCallback( - async (daimoPaymentResponse: any) => { - if (chargeDetails) { - dispatch(paymentActions.setIsDaimoPaymentProcessing(true)) - try { - // validate and parse destination chain id with proper fallback - // use chargeDetails chainId if it's a valid non-negative integer, otherwise use daimo response - const parsedChainId = Number(chargeDetails.chainId) - const destinationChainId = - Number.isInteger(parsedChainId) && parsedChainId >= 0 - ? parsedChainId - : Number(daimoPaymentResponse.payment.destination.chainId) - - const result = await completeDaimoPayment({ - chargeDetails: chargeDetails, - txHash: daimoPaymentResponse.txHash as string, - destinationchainId: destinationChainId, - payerAddress: peanutWalletAddress ?? daimoPaymentResponse.payment.source.payerAddress, - sourceChainId: daimoPaymentResponse.payment.source.chainId, - sourceTokenAddress: daimoPaymentResponse.payment.source.tokenAddress, - sourceTokenSymbol: daimoPaymentResponse.payment.source.tokenSymbol, - }) - - if (result.status === 'Success') { - dispatch(paymentActions.setView('STATUS')) - } else if (result.status === 'Charge Created') { - dispatch(paymentActions.setView('CONFIRM')) - } else if (result.status === 'Error') { - console.error('Payment initiation failed:', result.error) - } else { - console.warn('Unexpected status from usePaymentInitiator:', result.status) - } - } catch (e) { - console.error('Error completing daimo payment:', e) - } finally { - dispatch(paymentActions.setIsDaimoPaymentProcessing(false)) - } - } - }, - [chargeDetails, completeDaimoPayment, dispatch, peanutWalletAddress] - ) - - if (!method || !parsedPaymentData) return null - - return ( - - { - // First check if parent wants to intercept (e.g. show balance modal) - if (onBeforeShow) { - const shouldProceed = await onBeforeShow() - if (!shouldProceed) { - return false - } - } - - // Then check invite modal - if (!confirmLoseInvite && showConfirmModal) { - setShowInviteModal(true) - return false - } - - // Finally initiate payment - return await handleInitiateDaimoPayment() - }} - disabled={!usdAmount} - minAmount={0.1} - maxAmount={30_000} - loading={isProcessing} - onValidationError={(error) => { - dispatch(paymentActions.setDaimoError(error)) - }} - > - {({ onClick, loading }) => { - // Store the onClick function so we can trigger it from elsewhere - daimoPayButtonClickRef.current = onClick - // also store in parent ref if provided (for balance modal in ActionList) - if (clickHandlerRef) { - clickHandlerRef.current = onClick - } - - return ( - } - /> - ) - }} - - - { - setShowInviteModal(false) - setConfirmLoseInvite(true) - // Directly initiate the Daimo payment instead of triggering button click - const success = await handleInitiateDaimoPayment() - if (success && daimoPayButtonClickRef.current) { - // Only trigger the actual Daimo widget if payment initiation was successful - daimoPayButtonClickRef.current() - } - }} - isOpen={showInviteModal} - onClose={() => { - setShowInviteModal(false) - // Reset confirmLoseInvite when modal is closed without proceeding - setConfirmLoseInvite(false) - }} - /> - - ) -} - -export default ActionListDaimoPayButton diff --git a/src/components/Common/ContactsListSkeleton.tsx b/src/components/Common/ContactsListSkeleton.tsx new file mode 100644 index 000000000..9f6ba2ced --- /dev/null +++ b/src/components/Common/ContactsListSkeleton.tsx @@ -0,0 +1,28 @@ +'use client' +import { ActionListCard } from '../ActionListCard' +import { getCardPosition } from '../Global/Card' + +/** + * displays a contacts list skeleton during loading + */ +export const ContactsListSkeleton = ({ count = 10 }: { count?: number }) => { + return ( +
+

Your contacts

+
+ {Array.from({ length: count }).map((_, index) => { + const position = getCardPosition(index, count) + return ( + } + position={position} + onClick={() => {}} + leftIcon={
} + /> + ) + })} +
+
+ ) +} diff --git a/src/components/Common/CountryList.tsx b/src/components/Common/CountryList.tsx index 1f40a8a86..839f735d7 100644 --- a/src/components/Common/CountryList.tsx +++ b/src/components/Common/CountryList.tsx @@ -1,11 +1,9 @@ 'use client' import { - BRIDGE_ALPHA3_TO_ALPHA2, type CountryData, countryData, MantecaSupportedExchanges, ALL_COUNTRIES_ALPHA3_TO_ALPHA2, - NON_EUR_SEPA_ALPHA2, } from '@/components/AddMoney/consts' import EmptyState from '@/components/Global/EmptyStates/EmptyState' import { SearchInput } from '@/components/SearchInput' @@ -165,9 +163,7 @@ export const CountryList = ({ } else if (viewMode === 'claim-request') { // support bridge or manteca supported countries, but temporarily disable sepa corridors // where local currency is not eur (show as soon) - const isDisabledNonEurSepa = NON_EUR_SEPA_ALPHA2.has(twoLetterCountryCode.toUpperCase()) - const isBridgeAndNotDisabled = isBridgeSupportedCountry && !isDisabledNonEurSepa - isSupported = isBridgeAndNotDisabled || isMantecaSupportedCountry + isSupported = isBridgeSupportedCountry || isMantecaSupportedCountry } else { // support all countries isSupported = true diff --git a/src/components/Common/CountryListRouter.tsx b/src/components/Common/CountryListRouter.tsx index c5b46624a..fdc1953c8 100644 --- a/src/components/Common/CountryListRouter.tsx +++ b/src/components/Common/CountryListRouter.tsx @@ -1,9 +1,6 @@ 'use client' import NavHeader from '@/components/Global/NavHeader' -import PeanutActionDetailsCard, { - type PeanutActionDetailsCardRecipientType, - type PeanutActionDetailsCardTransactionType, -} from '@/components/Global/PeanutActionDetailsCard' +import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard' import { ClaimBankFlowStep, useClaimBankFlow } from '@/context/ClaimBankFlowContext' import { formatUnits } from 'viem' import { formatTokenAmount, printableAddress } from '@/utils/general.utils' @@ -11,178 +8,62 @@ import { CountryList } from '@/components/Common/CountryList' import { type ClaimLinkData } from '@/services/sendLinks' import { type CountryData, MantecaSupportedExchanges } from '@/components/AddMoney/consts' import useSavedAccounts from '@/hooks/useSavedAccounts' -import { type ParsedURL } from '@/lib/url-parser/types/payment' import { useCallback, useMemo } from 'react' -import { RequestFulfillmentBankFlowStep, useRequestFulfillmentFlow } from '@/context/RequestFulfillmentFlowContext' -import { usePaymentStore } from '@/redux/hooks' -import { useAuth } from '@/context/authContext' -import { BankRequestType, useDetermineBankRequestType } from '@/hooks/useDetermineBankRequestType' interface ICountryListRouterViewProps { - claimLinkData?: ClaimLinkData + claimLinkData: ClaimLinkData inputTitle: string - requestLinkData?: ParsedURL - flow: 'claim' | 'request' } /** - * Used to display countries list for claim link and request flow with @PeanutActionDetailsCard component as header - * - * @param {object} props - * @param {'claim' | 'request'} props.flow The flow type (claim or request) - * @param {ClaimLinkData} props.claimLinkData The claim link data - * @param {ParsedURL} props.requestLinkData The request link data - * @param {string} props.inputTitle The input title to be passed to @CountryList component - * @returns {JSX.Element} + * displays countries list for claim link flow with @PeanutActionDetailsCard component as header */ -export const CountryListRouter = ({ - flow, - claimLinkData, - requestLinkData, - inputTitle, -}: ICountryListRouterViewProps) => { +export const CountryListRouter = ({ claimLinkData, inputTitle }: ICountryListRouterViewProps) => { const { setFlowStep: setClaimBankFlowStep, setSelectedCountry, setClaimToMercadoPago, setFlowStep, } = useClaimBankFlow() - const { - setFlowStep: setRequestFulfilmentBankFlowStep, - setShowRequestFulfilmentBankFlowManager, - setSelectedCountry: setSelectedCountryForRequest, - setShowVerificationModal, - setFulfillUsingManteca, - } = useRequestFulfillmentFlow() const savedAccounts = useSavedAccounts() - const { chargeDetails } = usePaymentStore() - const { requestType } = useDetermineBankRequestType(chargeDetails?.requestLink.recipientAccount.userId ?? '') - const { user } = useAuth() const handleCountryClick = (country: CountryData) => { const isMantecaSupportedCountry = Object.keys(MantecaSupportedExchanges).includes(country.id) - if (flow === 'claim') { - setSelectedCountry(country) - if (isMantecaSupportedCountry) { - setFlowStep(null) // reset the flow step to initial view first - setClaimToMercadoPago(true) - } else { - setClaimBankFlowStep(ClaimBankFlowStep.BankDetailsForm) - } - } else if (flow === 'request') { - if (isMantecaSupportedCountry) { - setShowRequestFulfilmentBankFlowManager(false) - setFulfillUsingManteca(true) - } - setSelectedCountryForRequest(country) - if (requestType === BankRequestType.PayerKycNeeded) { - if (user && (!user.user.fullName || !user.user.email)) { - setRequestFulfilmentBankFlowStep(RequestFulfillmentBankFlowStep.CollectUserDetails) - } else { - setShowVerificationModal(true) - } - } else { - setRequestFulfilmentBankFlowStep(RequestFulfillmentBankFlowStep.OnrampConfirmation) - } + setSelectedCountry(country) + if (isMantecaSupportedCountry) { + setFlowStep(null) // reset the flow step to initial view first + setClaimToMercadoPago(true) + } else { + setClaimBankFlowStep(ClaimBankFlowStep.BankDetailsForm) } } - const receipientType = useCallback((): PeanutActionDetailsCardRecipientType => { - switch (flow) { - case 'claim': - return 'USERNAME' - case 'request': { - if (requestLinkData?.recipient?.recipientType === 'USERNAME') { - return 'USERNAME' - } else if (requestLinkData?.recipient?.recipientType === 'ADDRESS') { - return 'ADDRESS' - } else if (requestLinkData?.recipient?.recipientType === 'ENS') { - return 'ENS' - } - return 'USERNAME' - } - } - }, [flow, requestLinkData]) - - const receipientName = useCallback(() => { - switch (flow) { - case 'claim': - return claimLinkData?.sender?.username ?? printableAddress(claimLinkData?.senderAddress ?? '') - case 'request': - if (chargeDetails?.requestLink.recipientAccount.type === 'peanut-wallet') { - return ( - chargeDetails?.requestLink.recipientAccount.user.username ?? - printableAddress(chargeDetails?.requestLink.recipientAddress as string) - ) - } else { - return printableAddress(chargeDetails?.requestLink.recipientAccount.identifier as string) - } - } - }, [flow, claimLinkData, chargeDetails]) - - const amount = useCallback(() => { - switch (flow) { - case 'claim': - return formatTokenAmount( - Number(formatUnits(claimLinkData?.amount ?? 0n, claimLinkData?.tokenDecimals ?? 0)) - ) - case 'request': - return chargeDetails?.tokenAmount ?? '0' - } - }, [flow, claimLinkData, chargeDetails]) - - const tokenSymbol = useCallback(() => { - switch (flow) { - case 'claim': - return claimLinkData?.tokenSymbol ?? requestLinkData?.token?.symbol - case 'request': - return chargeDetails?.tokenSymbol - } - }, [flow, claimLinkData, requestLinkData, chargeDetails]) + const recipientName = useMemo(() => { + return claimLinkData?.sender?.username ?? printableAddress(claimLinkData?.senderAddress ?? '') + }, [claimLinkData]) - const peanutActionDetailsCardProps = useMemo(() => { - const transactionType: PeanutActionDetailsCardTransactionType = - flow === 'claim' ? 'CLAIM_LINK' : 'REQUEST_PAYMENT' - - return { - avatarSize: 'small', - transactionType, - recipientType: receipientType(), - recipientName: receipientName(), - amount: amount(), - tokenSymbol: tokenSymbol(), - } - }, [flow, claimLinkData, requestLinkData, chargeDetails]) + const amount = useMemo(() => { + return formatTokenAmount(Number(formatUnits(claimLinkData?.amount ?? 0n, claimLinkData?.tokenDecimals ?? 0))) + }, [claimLinkData]) const onPrev = useCallback(() => { - if (flow === 'claim') { - if (savedAccounts.length > 0) { - setClaimBankFlowStep(ClaimBankFlowStep.SavedAccountsList) - } else { - setClaimBankFlowStep(null) - } - } else if (flow === 'request') { - setRequestFulfilmentBankFlowStep(null) - setShowRequestFulfilmentBankFlowManager(false) + if (savedAccounts.length > 0) { + setClaimBankFlowStep(ClaimBankFlowStep.SavedAccountsList) + } else { + setClaimBankFlowStep(null) } - }, [ - flow, - savedAccounts, - setClaimBankFlowStep, - setRequestFulfilmentBankFlowStep, - setShowRequestFulfilmentBankFlowManager, - ]) + }, [savedAccounts, setClaimBankFlowStep]) return (
- +
diff --git a/src/components/Common/SavedAccountsView.tsx b/src/components/Common/SavedAccountsView.tsx index ff1c95e5c..9d2a4acf8 100644 --- a/src/components/Common/SavedAccountsView.tsx +++ b/src/components/Common/SavedAccountsView.tsx @@ -7,7 +7,7 @@ import { Icon } from '@/components/Global/Icons/Icon' import NavHeader from '../Global/NavHeader' import Divider from '../0_Bruddle/Divider' -import { Button } from '../0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { ActionListCard } from '../ActionListCard' interface SavedAccountListProps { @@ -119,7 +119,7 @@ export function SavedAccountsMapping({ /> )}
- +
} diff --git a/src/components/Create/Create.utils.ts b/src/components/Create/Create.utils.ts index 6e9b6d517..a52ce9b94 100644 --- a/src/components/Create/Create.utils.ts +++ b/src/components/Create/Create.utils.ts @@ -1,8 +1,8 @@ import peanut from '@squirrel-labs/peanut-sdk' -import { peanutTokenDetails } from '@/constants' import { type IUserBalance } from '@/interfaces' -import { areEvmAddressesEqual, isNativeCurrency } from '@/utils' +import { areEvmAddressesEqual, isNativeCurrency } from '@/utils/general.utils' +import { peanutTokenDetails } from '@/constants/general.consts' export const isGaslessDepositPossible = ({ tokenAddress, diff --git a/src/components/Create/useCreateLink.tsx b/src/components/Create/useCreateLink.tsx index 59e9c62fd..c9888791a 100644 --- a/src/components/Create/useCreateLink.tsx +++ b/src/components/Create/useCreateLink.tsx @@ -1,8 +1,7 @@ 'use client' import { getLinkFromTx, getNextDepositIndex } from '@/app/actions/claimLinks' -import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN, next_proxy_url } from '@/constants' import { loadingStateContext, tokenSelectorContext } from '@/context' -import { isNativeCurrency, saveToLocalStorage } from '@/utils' +import { isNativeCurrency, saveToLocalStorage } from '@/utils/general.utils' import peanut, { generateKeysFromString, getLatestContractVersion, @@ -17,6 +16,8 @@ import { useSignTypedData } from 'wagmi' import { useZeroDev } from '@/hooks/useZeroDev' import { useWallet } from '@/hooks/wallet/useWallet' +import { next_proxy_url } from '@/constants/general.consts' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants/zerodev.consts' export const useCreateLink = () => { const { setLoadingState } = useContext(loadingStateContext) diff --git a/src/components/CrispChat.tsx b/src/components/CrispChat.tsx index 2dd6106b2..f4563448f 100644 --- a/src/components/CrispChat.tsx +++ b/src/components/CrispChat.tsx @@ -1,6 +1,6 @@ 'use client' -import { useSupportModalContext } from '@/context/SupportModalContext' +import { useModalsContext } from '@/context/ModalsContext' /** * Button component that opens the support drawer @@ -10,7 +10,7 @@ import { useSupportModalContext } from '@/context/SupportModalContext' * page layout interference. */ export const CrispButton = ({ children, ...rest }: React.HTMLAttributes) => { - const { setIsSupportModalOpen } = useSupportModalContext() + const { setIsSupportModalOpen } = useModalsContext() const handleClick = () => { setIsSupportModalOpen(true) diff --git a/src/components/Global/ActionModal/index.tsx b/src/components/Global/ActionModal/index.tsx index c872fb9ac..d3e3e2836 100644 --- a/src/components/Global/ActionModal/index.tsx +++ b/src/components/Global/ActionModal/index.tsx @@ -197,7 +197,7 @@ const ActionModal: React.FC = ({ {children} {btnIcon && currentIconPosition === 'left' && ( @@ -205,7 +205,7 @@ const ActionModal: React.FC = ({ {text} {btnIcon && currentIconPosition === 'right' && ( diff --git a/src/components/Global/AddressLink/index.tsx b/src/components/Global/AddressLink/index.tsx index 66cff9cf9..4953339c1 100644 --- a/src/components/Global/AddressLink/index.tsx +++ b/src/components/Global/AddressLink/index.tsx @@ -1,4 +1,4 @@ -import { printableAddress } from '@/utils' +import { printableAddress, isCryptoAddress } from '@/utils/general.utils' import { usePrimaryName } from '@justaname.id/react' import Link from 'next/link' import { useEffect, useState } from 'react' @@ -14,11 +14,11 @@ interface AddressLinkProps { const AddressLink = ({ address, className = '', isLink = true }: AddressLinkProps) => { const [displayAddress, setDisplayAddress] = useState( - isAddress(address) ? printableAddress(address) : address + isCryptoAddress(address) ? printableAddress(address) : address ) const [urlAddress, setUrlAddress] = useState(address) - // Look up ENS name only for Ethereum addresses + // Look up ENS name only for Ethereum addresses (ENS doesn't apply to Solana/Tron) const { primaryName: ensName } = usePrimaryName({ address: isAddress(address) ? (address as `0x${string}`) : undefined, chainId: 1, // Mainnet for ENS lookups @@ -26,7 +26,7 @@ const AddressLink = ({ address, className = '', isLink = true }: AddressLinkProp }) useEffect(() => { - // Update display: prefer ENS name for addresses, otherwise use as-is + // Update display: prefer ENS name for EVM addresses, otherwise shorten any crypto address if (isAddress(address) && ensName) { // for peanut ens names, strip the domain from the displayed string so its just a username (no ens subdomain) const peanutEnsDomain = process.env.NEXT_PUBLIC_JUSTANAME_ENS_DOMAIN || '' @@ -35,7 +35,7 @@ const AddressLink = ({ address, className = '', isLink = true }: AddressLinkProp setDisplayAddress(normalizedEnsName) setUrlAddress(normalizedEnsName) } else { - setDisplayAddress(isAddress(address) ? printableAddress(address) : address) + setDisplayAddress(isCryptoAddress(address) ? printableAddress(address) : address) } }, [address, ensName]) diff --git a/src/components/Global/AmountInput/index.tsx b/src/components/Global/AmountInput/index.tsx new file mode 100644 index 000000000..d8af865d0 --- /dev/null +++ b/src/components/Global/AmountInput/index.tsx @@ -0,0 +1,309 @@ +'use client' + +import { formatTokenAmount } from '@/utils/general.utils' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { twMerge } from 'tailwind-merge' +import { Icon as IconComponent } from '@/components/Global/Icons/Icon' +import { Slider } from '../Slider' +import { DeviceType, useDeviceType } from '@/hooks/useGetDeviceType' + +// Used for internal calculations, not displayed to the user +const DECIMAL_SCALE = 18 // Max expected decimal places for any denomination + +interface AmountInputProps { + className?: string + initialAmount?: string + onSubmit?: () => void + setPrimaryAmount: (value: string) => void + setSecondaryAmount?: (value: string) => void + onBlur?: () => void + disabled?: boolean + primaryDenomination?: { symbol: string; price: number; decimals: number } + secondaryDenomination?: { symbol: string; price: number; decimals: number } + setCurrentDenomination?: (denomination: string) => void + walletBalance?: string + hideCurrencyToggle?: boolean + hideBalance?: boolean + infoContent?: React.ReactNode + + showSlider?: boolean + maxAmount?: number + amountCollected?: number + defaultSliderValue?: number + defaultSliderSuggestedAmount?: number +} + +const AmountInput = ({ + className, + initialAmount, + onSubmit, + setPrimaryAmount, + setSecondaryAmount, + onBlur, + disabled, + primaryDenomination = { symbol: '$', price: 1, decimals: 2 }, + secondaryDenomination, + setCurrentDenomination, + walletBalance, + hideCurrencyToggle, + hideBalance, + infoContent, + + showSlider = false, + maxAmount, + amountCollected = 0, + defaultSliderValue, + defaultSliderSuggestedAmount, +}: AmountInputProps) => { + const [isFocused, setIsFocused] = useState(false) + const { deviceType } = useDeviceType() + // Only autofocus on desktop (WEB), not on mobile devices (IOS/ANDROID) + const shouldAutoFocus = deviceType === DeviceType.WEB + const showConversion = !hideCurrencyToggle && !!secondaryDenomination + + // Store display value for input field (what user sees when typing) + const [displayValue, setDisplayValue] = useState(initialAmount || '') + const [exactValue, setExactValue] = useState(Number(initialAmount || '') * 10 ** DECIMAL_SCALE) + const [displaySymbol, setDisplaySymbol] = useState(primaryDenomination.symbol) + + // Track when user is actively editing to prevent feedback loops from initialAmount sync + const isEditingRef = useRef(false) + + // Check if displayValue has a meaningful numeric value (not empty, "0", "0.00", etc.) + const hasValue = Boolean(Number(displayValue)) + + // Sync displayValue with initialAmount changes (e.g. when charge is fetched) + // Skip sync if user is actively editing to prevent overwriting their input + useEffect(() => { + if (initialAmount && initialAmount !== displayValue && !isEditingRef.current) { + setDisplayValue(initialAmount) + setExactValue(Number(initialAmount) * 10 ** DECIMAL_SCALE) + } + }, [initialAmount]) + + const denominations = { + [primaryDenomination.symbol]: primaryDenomination, + } + if (secondaryDenomination) { + denominations[secondaryDenomination.symbol] = secondaryDenomination + } + + const alternativeDisplaySymbol = useMemo(() => { + return Object.keys(denominations).find((key) => key !== displaySymbol) ?? '' + }, [displaySymbol]) + + useEffect(() => { + if (setCurrentDenomination) { + setCurrentDenomination(displaySymbol) + } + }, [displaySymbol]) + + /* + * Rate needed to convert from primary to secondary denomination by + * multiplying the primary rate by the exchange rate. + * Expressed as a integer with the scale of the max resolution denomination + */ + const exchangeRate = useMemo(() => { + if (!secondaryDenomination) return 1 + const alternativePrice = denominations[alternativeDisplaySymbol]?.price + const mainPrice = denominations[displaySymbol]?.price + return alternativePrice / mainPrice + }, [displaySymbol, alternativeDisplaySymbol, secondaryDenomination]) + + const alternativeValue = useMemo(() => { + if (!secondaryDenomination || !displayValue) return 0 + return exactValue * exchangeRate + }, [exactValue, secondaryDenomination, exchangeRate, displayValue]) + + const alternativeDisplayValue = useMemo(() => { + if (!secondaryDenomination || !alternativeValue) return '0.00' + const scaledDownValue = alternativeValue / 10 ** DECIMAL_SCALE + return formatTokenAmount(scaledDownValue, denominations[alternativeDisplaySymbol]?.decimals) ?? '0.00' + }, [alternativeValue, alternativeDisplaySymbol, secondaryDenomination]) + + useEffect(() => { + const isPrimaryDenomination = displaySymbol === primaryDenomination.symbol + // Strip commas before passing to consumers - they expect raw numeric strings + const rawDisplayValue = displayValue.replace(/,/g, '') + // Don't output "0.00" when there's no actual value - keep it empty to avoid feedback loops + const rawAlternativeValue = hasValue ? alternativeDisplayValue.replace(/,/g, '') : '' + + if (isPrimaryDenomination) { + setPrimaryAmount(rawDisplayValue) + setSecondaryAmount?.(rawAlternativeValue) + } else { + setPrimaryAmount(rawAlternativeValue) + setSecondaryAmount?.(rawDisplayValue) + } + }, [displayValue, alternativeDisplayValue, displaySymbol, secondaryDenomination, hasValue]) + + const onSliderValueChange = useCallback( + (value: number[]) => { + if (maxAmount) { + isEditingRef.current = true + const selectedPercentage = value[0] + let selectedAmount = (selectedPercentage / 100) * maxAmount + + // Only snap to exact remaining amount when user selects the 33.33% magnetic snap point + // This ensures equal splits fill the pot exactly to 100% + const SNAP_POINT_TOLERANCE = 0.5 // percentage points - allows magnetic snapping + const COMPLETION_THRESHOLD = 0.98 // 98% - if 33.33% would nearly complete pot + const EQUAL_SPLIT_PERCENTAGE = 100 / 3 // 33.333...% + + const isAt33SnapPoint = Math.abs(selectedPercentage - EQUAL_SPLIT_PERCENTAGE) < SNAP_POINT_TOLERANCE + if (isAt33SnapPoint && amountCollected > 0) { + const remainingAmount = maxAmount - amountCollected + // Only snap if there's remaining amount and 33.33% would nearly complete the pot + if (remainingAmount > 0 && selectedAmount >= remainingAmount * COMPLETION_THRESHOLD) { + selectedAmount = remainingAmount + } + } + + const selectedAmountStr = parseFloat(selectedAmount.toFixed(4)).toString() + const formattedAmount = formatTokenAmount( + selectedAmountStr, + denominations[displaySymbol]?.decimals, + true + ) + if (formattedAmount) { + setDisplayValue(formattedAmount) + setExactValue(Number(formattedAmount) * 10 ** DECIMAL_SCALE) + } + } + }, + [maxAmount, amountCollected] + ) + + // Sync default slider suggested amount to the input + useEffect(() => { + if (defaultSliderSuggestedAmount) { + const formattedAmount = formatTokenAmount(defaultSliderSuggestedAmount.toString(), 2) + if (formattedAmount) { + setDisplayValue(formattedAmount) + setExactValue(Number(formattedAmount) * 10 ** DECIMAL_SCALE) + } + } + }, [defaultSliderSuggestedAmount]) + + const inputRef = useRef(null) + // set input width based on display value length + // add extra space for decimal numbers to prevent cutoff + useEffect(() => { + if (inputRef.current) { + const length = displayValue?.length || 0 + // add 0.6ch extra width to prevent cutoff, minimum 4ch + const width = length ? `${length + 0.6}ch` : '4ch' + inputRef.current.style.width = width + } + }, [displayValue]) + + return ( +
inputRef.current?.focus()} + > +
+
+ + + {/* Input with fake caret */} +
+ { + isEditingRef.current = true + let value = e.target.value + const maxDecimals = denominations[displaySymbol].decimals + const formattedAmount = formatTokenAmount(value, maxDecimals, true) + if (formattedAmount !== undefined) { + value = formattedAmount + } + setDisplayValue(value) + setExactValue(Number(value) * 10 ** DECIMAL_SCALE) + }} + ref={inputRef} + inputMode="decimal" + type="text" + value={displayValue} + autoComplete="off" + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + if (onSubmit) onSubmit() + } + }} + onFocus={() => setIsFocused(true)} + onBlur={() => { + setIsFocused(false) + if (onBlur) onBlur() + }} + disabled={disabled} + /> + {/* Fake blinking caret shown when not focused and input is empty */} + {!isFocused && !displayValue && ( +
+ )} +
+
+ + {/* Conversion */} + {showConversion && ( + + )} + + {/* Balance */} + {walletBalance && !hideBalance && ( +
+ Balance: {secondaryDenomination ? 'USD ' : '$ '} + {walletBalance} +
+ )} +
+ {/* Conversion toggle */} + {showConversion && ( +
{ + e.preventDefault() + // Reset editing state - user is switching currency, allow sync with converted value + isEditingRef.current = false + // If no meaningful value entered, just switch symbol and keep empty + if (!hasValue) { + setDisplayValue('') + setExactValue(0) + setDisplaySymbol(alternativeDisplaySymbol) + return + } + setExactValue(alternativeValue) + setDisplayValue(alternativeDisplayValue.replace(/,/g, '')) + setDisplaySymbol(alternativeDisplaySymbol) + }} + > + +
+ )} + {infoContent} + {showSlider && maxAmount && ( +
+ +
+ )} + + ) +} + +export default AmountInput diff --git a/src/components/Global/Badges/StatusBadge.tsx b/src/components/Global/Badges/StatusBadge.tsx index 55c3ff751..8eaf7335d 100644 --- a/src/components/Global/Badges/StatusBadge.tsx +++ b/src/components/Global/Badges/StatusBadge.tsx @@ -1,7 +1,16 @@ import React from 'react' import { twMerge } from 'tailwind-merge' -export type StatusType = 'completed' | 'pending' | 'failed' | 'cancelled' | 'soon' | 'processing' | 'custom' | 'closed' +export type StatusType = + | 'completed' + | 'pending' + | 'failed' + | 'cancelled' + | 'soon' + | 'processing' + | 'custom' + | 'closed' + | 'refunded' interface StatusBadgeProps { status: StatusType @@ -21,6 +30,7 @@ const StatusBadge: React.FC = ({ status, className, size = 'sm return 'bg-secondary-4 text-yellow-6 border border-yellow-7' case 'failed': case 'cancelled': + case 'refunded': return 'bg-error-1 text-error border border-error-2' case 'soon': case 'custom': @@ -42,6 +52,8 @@ const StatusBadge: React.FC = ({ status, className, size = 'sm return 'Failed' case 'cancelled': return 'Cancelled' + case 'refunded': + return 'Refunded' case 'soon': return 'Soon!' case 'closed': diff --git a/src/components/Global/Banner/index.tsx b/src/components/Global/Banner/index.tsx index 8ccb19e83..1204c9b1e 100644 --- a/src/components/Global/Banner/index.tsx +++ b/src/components/Global/Banner/index.tsx @@ -6,7 +6,7 @@ import { MarqueeWrapper } from '../MarqueeWrapper' import maintenanceConfig from '@/config/underMaintenance.config' import { HandThumbsUp } from '@/assets' import Image from 'next/image' -import { useSupportModalContext } from '@/context/SupportModalContext' +import { useModalsContext } from '@/context/ModalsContext' import { GIT_COMMIT_HASH, IS_PRODUCTION } from '@/constants/general.consts' export function Banner() { @@ -26,7 +26,7 @@ export function Banner() { } function FeedbackBanner() { - const { setIsSupportModalOpen } = useSupportModalContext() + const { setIsSupportModalOpen } = useModalsContext() const handleClick = () => { setIsSupportModalOpen(true) diff --git a/src/components/Global/ChainSelector/index.tsx b/src/components/Global/ChainSelector/index.tsx deleted file mode 100644 index 919e4bf34..000000000 --- a/src/components/Global/ChainSelector/index.tsx +++ /dev/null @@ -1,151 +0,0 @@ -'use client' - -import { supportedPeanutChains } from '@/constants' -import { tokenSelectorContext } from '@/context' -import { type IPeanutChainDetails, type IUserBalance } from '@/interfaces' -import { calculateValuePerChain, formatTokenAmount } from '@/utils' -import { Menu, Transition } from '@headlessui/react' -import { useContext, useMemo, useState } from 'react' -import Icon from '../Icon' -import Search from '../Search' - -type Chain = { - name: string - icon: { - url: string - format: string - } - mainnet: boolean -} - -interface IChainSelectorProps { - chainsToDisplay?: IPeanutChainDetails[] - onChange?: (chainId: string) => void - balances: IUserBalance[] -} - -const ChainSelector = ({ chainsToDisplay, onChange, balances }: IChainSelectorProps) => { - const [, setVisible] = useState(false) - const [filterValue, setFilterValue] = useState('') - - const { selectedChainID, setSelectedChainID } = useContext(tokenSelectorContext) - - const valuePerChain = useMemo(() => calculateValuePerChain(balances), [balances]) - - const _chainsToDisplay = useMemo(() => { - let chains - if (chainsToDisplay) { - chains = chainsToDisplay - } else { - chains = supportedPeanutChains - } - if (valuePerChain.length > 0) { - // Sort the chains based on the value - chains = chains.sort((a, b) => { - const aValue = valuePerChain.find((value) => value.chainId === a.chainId)?.valuePerChain || 0 - const bValue = valuePerChain.find((value) => value.chainId === b.chainId)?.valuePerChain || 0 - return bValue - aValue - }) - } - if (filterValue) { - chains = chains.filter( - (chain) => - chain.name.toLowerCase().includes(filterValue.toLowerCase()) || - chain.shortName.toLowerCase().includes(filterValue.toLowerCase()) - ) - } - return chains - }, [filterValue, valuePerChain, chainsToDisplay]) - - function setChain(chainId: string): void { - setSelectedChainID(chainId) - setVisible(false) - onChange?.(chainId) - } - - return ( - - {({ open }) => ( - <> - - chain.chainId === selectedChainID)?.icon.url} - alt={''} - className="h-6 w-6" - /> - - - -
- - -
- setFilterValue(e.target.value)} - onSubmit={() => {}} - medium - /> -
- -
- {_chainsToDisplay.map( - (chain) => - chain.mainnet && - chainItem({ - chain, - setChain: () => setChain(chain.chainId), - valuePerChain: !chainsToDisplay - ? valuePerChain.find((value) => value.chainId === chain.chainId) - ?.valuePerChain - : undefined, - }) - )} -
-
-
-
- - )} -
- ) -} - -const chainItem = ({ - chain, - setChain, - valuePerChain, -}: { - chain: Chain - setChain: () => void - valuePerChain?: number -}) => { - return ( - -
- {chain.name} -
{chain.name}
-
- {valuePerChain &&
${formatTokenAmount(valuePerChain, 2)}
} -
- ) -} - -export default ChainSelector diff --git a/src/components/Global/ConfirmDetails/Index.tsx b/src/components/Global/ConfirmDetails/Index.tsx deleted file mode 100644 index 57e09e99b..000000000 --- a/src/components/Global/ConfirmDetails/Index.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { formatAmount } from '@/utils' -import Icon from '../Icon' - -interface IConfirmDetailsProps { - tokenSymbol: string - tokenIconUri: string - chainName: string - chainIconUri: string - tokenAmount: string - tokenPrice?: number - title?: string - showOnlyUSD?: boolean -} - -// @dev TODO: makes no sense to have 1 component thats only used in 1 view. Better to have inline then. -// This component should be used in pay confirm too! -const ConfirmDetails = ({ - tokenSymbol, - tokenIconUri, - chainName, - chainIconUri, - title, - tokenAmount, - tokenPrice, - showOnlyUSD, -}: IConfirmDetailsProps) => { - return ( -
- {title && } -
- {showOnlyUSD ? ( -
- - From Peanut Wallet -
- ) : ( - <> -
- {tokenIconUri ? ( - - ) : ( - - )} - -
- {tokenPrice && ( - - )} - - )} -
- {!showOnlyUSD && ( -
- {chainIconUri ? ( - - ) : ( - - )} - -
- )} -
- ) -} - -export default ConfirmDetails diff --git a/src/components/Global/ConfirmInviteModal/index.tsx b/src/components/Global/ConfirmInviteModal/index.tsx index 57f4f16bc..eccaf540e 100644 --- a/src/components/Global/ConfirmInviteModal/index.tsx +++ b/src/components/Global/ConfirmInviteModal/index.tsx @@ -1,9 +1,9 @@ 'use client' -import React, { type FC } from 'react' +import { type FC } from 'react' import Image from 'next/image' import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets' import Modal from '../Modal' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import chillPeanutAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_01.gif' interface ConfirmInviteModalProps { diff --git a/src/components/Global/Contributors/ContributorCard.tsx b/src/components/Global/Contributors/ContributorCard.tsx index e31207355..5ee49a55c 100644 --- a/src/components/Global/Contributors/ContributorCard.tsx +++ b/src/components/Global/Contributors/ContributorCard.tsx @@ -4,7 +4,7 @@ import Card, { type CardPosition } from '../Card' import AvatarWithBadge from '@/components/Profile/AvatarWithBadge' import { getColorForUsername } from '@/utils/color.utils' import { VerifiedUserLabel } from '@/components/UserHeader' -import { formatTokenAmount } from '@/utils' +import { formatTokenAmount } from '@/utils/general.utils' import { isAddress } from 'viem' import { useRouter } from 'next/navigation' import { twMerge } from 'tailwind-merge' diff --git a/src/components/Global/Contributors/index.tsx b/src/components/Global/Contributors/index.tsx index a8844fa7c..ff8d3b2be 100644 --- a/src/components/Global/Contributors/index.tsx +++ b/src/components/Global/Contributors/index.tsx @@ -1,6 +1,5 @@ import { type ChargeEntry } from '@/services/services.types' -import { getContributorsFromCharge } from '@/utils' -import React from 'react' +import { getContributorsFromCharge } from '@/utils/general.utils' import ContributorCard from './ContributorCard' import { getCardPosition } from '../Card' diff --git a/src/components/Global/CopyField/index.tsx b/src/components/Global/CopyField/index.tsx index 8c7bd8e0f..64c2a940e 100644 --- a/src/components/Global/CopyField/index.tsx +++ b/src/components/Global/CopyField/index.tsx @@ -1,7 +1,7 @@ 'use client' -import { Button, type ButtonVariant } from '@/components/0_Bruddle' +import { Button, type ButtonVariant } from '@/components/0_Bruddle/Button' import BaseInput from '@/components/0_Bruddle/BaseInput' -import { copyTextToClipboardWithFallback } from '@/utils' +import { copyTextToClipboardWithFallback } from '@/utils/general.utils' import { useCallback, useState } from 'react' import { twMerge } from 'tailwind-merge' diff --git a/src/components/Global/CopyToClipboard/index.tsx b/src/components/Global/CopyToClipboard/index.tsx index 69f28a1c6..d5aaa8d8f 100644 --- a/src/components/Global/CopyToClipboard/index.tsx +++ b/src/components/Global/CopyToClipboard/index.tsx @@ -1,7 +1,11 @@ -import React, { useState } from 'react' +import React, { useState, forwardRef, useImperativeHandle, useCallback } from 'react' import { twMerge } from 'tailwind-merge' import { Icon } from '../Icons/Icon' -import { Button, type ButtonSize } from '@/components/0_Bruddle' +import { Button, type ButtonSize } from '@/components/0_Bruddle/Button' + +export interface CopyToClipboardRef { + copy: () => void +} interface Props { textToCopy: string @@ -12,43 +16,62 @@ interface Props { buttonSize?: ButtonSize } -const CopyToClipboard = ({ textToCopy, fill, className, iconSize = '6', type = 'icon', buttonSize }: Props) => { - const [copied, setCopied] = useState(false) +const CopyToClipboard = forwardRef( + ({ textToCopy, fill = 'black', className, iconSize = '6', type = 'icon', buttonSize }, ref) => { + const [copied, setCopied] = useState(false) - const handleCopy = (e: React.MouseEvent) => { - e.stopPropagation() - navigator.clipboard.writeText(textToCopy).then(() => { - setCopied(true) - setTimeout(() => setCopied(false), 2000) - }) - } + const copy = useCallback(() => { + navigator.clipboard.writeText(textToCopy).then(() => { + setCopied(true) + setTimeout(() => setCopied(false), 2000) + }) + }, [textToCopy]) + + useImperativeHandle(ref, () => ({ copy }), [copy]) + + const handleCopy = (e: React.MouseEvent) => { + e.stopPropagation() + copy() + } + + // convert tailwind size to pixels (2=8px, 3=12px, 4=16px, 6=24px, 8=32px) + const sizeMap: Record = { + '2': 8, + '3': 12, + '4': 16, + '6': 24, + '8': 32, + } + + const iconSizePx = sizeMap[iconSize] || 24 + + if (type === 'button') { + return ( + + ) + } - if (type === 'button') { return ( - + /> ) } +) - return ( - - ) -} +CopyToClipboard.displayName = 'CopyToClipboard' export default CopyToClipboard diff --git a/src/components/Global/CreateAccountButton/index.tsx b/src/components/Global/CreateAccountButton/index.tsx index f367ad66d..7cd3e0c48 100644 --- a/src/components/Global/CreateAccountButton/index.tsx +++ b/src/components/Global/CreateAccountButton/index.tsx @@ -1,9 +1,8 @@ 'use client' import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import Image from 'next/image' -import React from 'react' interface CreateAccountButtonProps { onClick: () => void diff --git a/src/components/Global/DaimoPayButton/index.tsx b/src/components/Global/DaimoPayButton/index.tsx index cdb6b6732..a1982fb7e 100644 --- a/src/components/Global/DaimoPayButton/index.tsx +++ b/src/components/Global/DaimoPayButton/index.tsx @@ -1,8 +1,8 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { type IconName } from '@/components/Global/Icons/Icon' -import { PEANUT_WALLET_TOKEN } from '@/constants' +import { PEANUT_WALLET_TOKEN } from '@/constants/zerodev.consts' import { DaimoPayButton as DaimoPayButtonSDK, useDaimoPayUI } from '@daimo/pay' import { useCallback, useEffect } from 'react' import { getAddress } from 'viem' @@ -52,7 +52,7 @@ export interface DaimoPayButtonProps { onValidationError?: (error: string | null) => void } -export const DaimoPayButton = ({ +const DaimoPayButton = ({ amount, toAddress, toChainId, @@ -76,7 +76,7 @@ export const DaimoPayButton = ({ const handleClick = useCallback(async () => { // Parse and validate amount - const formattedAmount = parseFloat(amount.replace(/,/g, '')) + const formattedAmount = parseFloat(amount) // Validate amount range if specified if (minAmount !== undefined && formattedAmount < minAmount) { @@ -146,7 +146,7 @@ export const DaimoPayButton = ({ appId={daimoAppId} intent="Deposit" toChain={toChainId ?? arbitrum.id} // use provided chain or default to arbitrum - toUnits={amount.replace(/,/g, '')} + toUnits={amount} toAddress={getAddress(toAddress)} toToken={getAddress(toTokenAddress ?? PEANUT_WALLET_TOKEN)} // use provided token or default to usdc on arbitrum onPaymentCompleted={onPaymentCompleted} diff --git a/src/components/Global/DirectSendQR/index.tsx b/src/components/Global/DirectSendQR/index.tsx index d4cf7d182..599d310f6 100644 --- a/src/components/Global/DirectSendQR/index.tsx +++ b/src/components/Global/DirectSendQR/index.tsx @@ -1,18 +1,14 @@ 'use client' import { resolveEns } from '@/app/actions/ens' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import Checkbox from '@/components/0_Bruddle/Checkbox' import { useToast } from '@/components/0_Bruddle/Toast' import Modal from '@/components/Global/Modal' import QRBottomDrawer from '@/components/Global/QRBottomDrawer' -import PeanutLoading from '@/components/Global/PeanutLoading' // QRScanner is NOT lazy-loaded - critical path for payments, needs instant response // 50KB bundle cost is worth it for better UX on primary flow import QRScanner from '@/components/Global/QRScanner' import { useAuth } from '@/context/authContext' -import { usePush } from '@/context/pushProvider' -import { useAppDispatch } from '@/redux/hooks' -import { paymentActions } from '@/redux/slices/payment-slice' import { hitUserMetric } from '@/utils/metrics.utils' import * as Sentry from '@sentry/nextjs' import { usePathname, useRouter, useSearchParams } from 'next/navigation' @@ -22,7 +18,7 @@ import ActionModal from '../ActionModal' import { Icon, type IconName } from '../Icons/Icon' import { EQrType, NAME_BY_QR_TYPE, parseEip681, recognizeQr } from './utils' import { useHaptic } from 'use-haptic' -import { useQrCodeContext } from '@/context/QrCodeContext' +import { useModalsContext } from '@/context/ModalsContext' const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL! @@ -51,8 +47,9 @@ const MODAL_CONTENTS: Record> [EModalType.UNRECOGNIZED]: UnrecognizedContent, } +// note: push notifications are now handled by onesignal via useNotifications hook. +// this component just tracks the metric and shows confirmation. function NotSupportedContent({ setModalContent, qrType }: ModalContentProps) { - const pushNotifications = usePush() const { user } = useAuth() return (
@@ -60,15 +57,10 @@ function NotSupportedContent({ setModalContent, qrType }: ModalContentProps) { Get notified when it goes live! + icon={} + />
-
+
diff --git a/src/components/Global/ErrorAlert/index.tsx b/src/components/Global/ErrorAlert/index.tsx index e95e010e1..3115eb7fc 100644 --- a/src/components/Global/ErrorAlert/index.tsx +++ b/src/components/Global/ErrorAlert/index.tsx @@ -11,7 +11,7 @@ interface ErrorAlertProps { const ErrorAlert = ({ className, iconSize = 16, description, iconClassName }: ErrorAlertProps) => { return (
- +
{description}
) diff --git a/src/components/Global/ExchangeRateWidget/index.tsx b/src/components/Global/ExchangeRateWidget/index.tsx index 767e47626..912c4c869 100644 --- a/src/components/Global/ExchangeRateWidget/index.tsx +++ b/src/components/Global/ExchangeRateWidget/index.tsx @@ -1,12 +1,12 @@ import CurrencySelect from '@/components/LandingPage/CurrencySelect' -import { countryCurrencyMappings } from '@/constants/countryCurrencyMapping' +import countryCurrencyMappings from '@/constants/countryCurrencyMapping' import { useDebounce } from '@/hooks/useDebounce' import { useExchangeRate } from '@/hooks/useExchangeRate' import Image from 'next/image' import { useRouter, useSearchParams } from 'next/navigation' import { type FC, useCallback, useEffect, useMemo } from 'react' import { Icon, type IconName } from '../Icons/Icon' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' interface IExchangeRateWidgetProps { ctaLabel: string @@ -126,7 +126,7 @@ const ExchangeRateWidget: FC = ({ ctaLabel, ctaIcon, c setSelectedCurrency={setSourceCurrency} // excludeCurrencies={[destinationCurrency]} trigger={ - {openFaq === faq.id && ( @@ -108,9 +105,9 @@ export function FAQsPanel({ heading, questions }: FAQsProps) { ))} - +
- - +
+
) } diff --git a/src/components/Global/FeeDescription/index.tsx b/src/components/Global/FeeDescription/index.tsx deleted file mode 100644 index 5f90fabd0..000000000 --- a/src/components/Global/FeeDescription/index.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { formatAmount } from '@/utils' -import { useState } from 'react' -import Icon from '../Icon' -import InfoRow from '../InfoRow' - -const INITIAL_STATE = { - isExpanded: false, -} - -type TRange = { - min: number | string - max: number | string -} - -interface FeeDescriptionProps { - estimatedFee: number | string - networkFee: number | string - slippageRange?: TRange - minReceive?: string - maxSlippage?: string - accountTypeFee?: string - accountType?: string - loading?: boolean - isPromoApplied?: boolean -} - -const FeeDescription = ({ - estimatedFee, - networkFee, - minReceive, - maxSlippage, - accountTypeFee, - accountType, - loading, - isPromoApplied, - slippageRange, -}: FeeDescriptionProps) => { - const [toggleDetailedView, setToggleDetailedView] = useState(INITIAL_STATE) - - const handleExpandToggle = () => { - setToggleDetailedView((prev) => ({ isExpanded: !prev.isExpanded })) - } - - if (!slippageRange && !minReceive && !maxSlippage && !accountTypeFee) { - return ( -
- -
- ) - } - - return ( -
- {/* Header */} -
-
- - -
- {loading ? ( -
- ) : ( -
- - {parseFloat(estimatedFee.toString()) < 0.01 - ? '< $0.01' - : `~ $${formatAmount(estimatedFee)}`} - - -
- )} -
- - {/* Expandable Section */} -
-
- {!!minReceive && ( - - )} - - - - {!!slippageRange && ( - - )} - - {!!maxSlippage && parseFloat(maxSlippage) > 0 && ( - - )} - - {!!accountTypeFee && ( - - )} -
-
-
- ) -} - -export default FeeDescription diff --git a/src/components/Global/FlowHeader/index.tsx b/src/components/Global/FlowHeader/index.tsx index c8bb627b5..1d0125a35 100644 --- a/src/components/Global/FlowHeader/index.tsx +++ b/src/components/Global/FlowHeader/index.tsx @@ -1,4 +1,4 @@ -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { Icon } from '../Icons/Icon' import { type ReactNode } from 'react' diff --git a/src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx b/src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx index 5d22ad3b2..a7d94fd53 100644 --- a/src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx +++ b/src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx @@ -1,9 +1,10 @@ import { render, act } from '@testing-library/react' import GeneralRecipientInput from '../index' -import * as utils from '@/utils' +import * as bridgeUtils from '@/utils/bridge-accounts.utils' +import * as sentryUtils from '@/utils/sentry.utils' import * as ens from '@/app/actions/ens' import type { RecipientType } from '@/interfaces' -import { validateEnsName } from '@/utils' +import { validateEnsName } from '@/utils/general.utils' // Test case type definition for better maintainability type TestCase = { @@ -17,13 +18,17 @@ type TestCase = { } // Mock dependencies -jest.mock('@/utils', () => { - const actualUtils = jest.requireActual('@/utils') +jest.mock('@/utils/bridge-accounts.utils', () => ({ + validateBankAccount: jest.fn(), +})) +jest.mock('@/utils/sentry.utils', () => ({ + fetchWithSentry: jest.fn(), +})) +jest.mock('@/utils/general.utils', () => { + const actualUtils = jest.requireActual('@/utils/general.utils') return { - validateBankAccount: jest.fn(), + ...actualUtils, sanitizeBankAccount: (input: string) => input.toLowerCase().replace(/\s/g, ''), - validateEnsName: actualUtils.validateEnsName, // Use the actual implementation - fetchWithSentry: jest.fn(), } }) @@ -45,8 +50,8 @@ describe('GeneralRecipientInput Type Detection', () => { beforeEach(() => { onUpdateMock = jest.fn() jest.clearAllMocks() - ;(utils.validateBankAccount as jest.Mock).mockResolvedValue(true) - ;(utils.fetchWithSentry as jest.Mock).mockResolvedValue({ status: 404 }) + ;(bridgeUtils.validateBankAccount as jest.Mock).mockResolvedValue(true) + ;(sentryUtils.fetchWithSentry as jest.Mock).mockResolvedValue({ status: 404 }) }) const setup = async (initialValue = '', isWithdrawal = false) => { @@ -200,8 +205,18 @@ describe('GeneralRecipientInput Type Detection', () => { await setup(input) + // Wait for async validation to complete await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 0)) + await new Promise((resolve) => setTimeout(resolve, 100)) + }) + + // Wait for onUpdate to be called + await act(async () => { + let attempts = 0 + while (!onUpdateMock.mock.calls.length && attempts < 50) { + await new Promise((resolve) => setTimeout(resolve, 10)) + attempts++ + } }) expect(onUpdateMock).toHaveBeenCalledWith( @@ -217,7 +232,7 @@ describe('GeneralRecipientInput Type Detection', () => { ...(expectedError && { errorMessage: expectedError }), }) ) - }) + }, 10000) // 10 second timeout } ) }) diff --git a/src/components/Global/GeneralRecipientInput/index.tsx b/src/components/Global/GeneralRecipientInput/index.tsx index cf949d06e..969ada512 100644 --- a/src/components/Global/GeneralRecipientInput/index.tsx +++ b/src/components/Global/GeneralRecipientInput/index.tsx @@ -1,13 +1,13 @@ 'use client' import ValidatedInput, { type InputUpdate } from '@/components/Global/ValidatedInput' import * as interfaces from '@/interfaces' -import { validateBankAccount } from '@/utils' +import { validateBankAccount } from '@/utils/bridge-accounts.utils' import { formatBankAccountDisplay, sanitizeBankAccount } from '@/utils/format.utils' import * as Senty from '@sentry/nextjs' import { useCallback, useRef } from 'react' import { isIBAN } from 'validator' import { validateAndResolveRecipient } from '@/lib/validation/recipient' -import { BASE_URL } from '@/constants' +import { BASE_URL } from '@/constants/general.consts' type GeneralRecipientInputProps = { className?: string @@ -40,56 +40,50 @@ const GeneralRecipientInput = ({ const errorMessage = useRef('') const resolvedAddress = useRef('') - const checkAddress = useCallback(async (recipient: string): Promise => { - try { - let isValid = false - let type: interfaces.RecipientType = 'address' + const checkAddress = useCallback( + async (recipient: string): Promise => { + try { + let isValid = false + let type: interfaces.RecipientType = 'address' - // First trim the input, then strip off the Peanut ENS domain from the end if it exists - let processedInput = recipient.trim().replace(`${BASE_URL}/`, '') - - if (process.env.NEXT_PUBLIC_JUSTANAME_ENS_DOMAIN) { - const domainSuffix = `.${process.env.NEXT_PUBLIC_JUSTANAME_ENS_DOMAIN}` - //regex to safely remove domain from end only (e.g., ".testvc.eth" from "user.testvc.eth") - const domainRegex = new RegExp(domainSuffix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '$', 'i') - processedInput = processedInput.replace(domainRegex, '') - } - - const trimmedInput = processedInput - const sanitizedInput = sanitizeBankAccount(trimmedInput) - - if (isIBAN(sanitizedInput)) { - type = 'iban' - isValid = await validateBankAccount(sanitizedInput) - if (!isValid) errorMessage.current = 'Invalid IBAN, country not supported' - } else if (/^[0-9]{1,17}$/.test(sanitizedInput)) { - type = 'us' - isValid = true - } else { - try { - const validation = await validateAndResolveRecipient(trimmedInput, isWithdrawal) + // trim the input and remove URL prefix if present + const trimmedInput = recipient.trim().replace(`${BASE_URL}/`, '') + const sanitizedInput = sanitizeBankAccount(trimmedInput) + if (isIBAN(sanitizedInput)) { + type = 'iban' + isValid = await validateBankAccount(sanitizedInput) + if (!isValid) errorMessage.current = 'Invalid IBAN, country not supported' + } else if (/^[0-9]{1,17}$/.test(sanitizedInput)) { + type = 'us' isValid = true - resolvedAddress.current = validation.resolvedAddress - type = validation.recipientType.toLowerCase() as interfaces.RecipientType - } catch (error: unknown) { - errorMessage.current = (error as Error).message - // For withdrawal context, failed non-address inputs should be treated as ENS - if (isWithdrawal && !trimmedInput.startsWith('0x')) { - type = 'ens' + } else { + try { + const validation = await validateAndResolveRecipient(trimmedInput, isWithdrawal) + + isValid = true + resolvedAddress.current = validation.resolvedAddress + type = validation.recipientType.toLowerCase() as interfaces.RecipientType + } catch (error: unknown) { + errorMessage.current = (error as Error).message + // For withdrawal context, failed non-address inputs should be treated as ENS + if (isWithdrawal && !trimmedInput.startsWith('0x')) { + type = 'ens' + } + recipientType.current = type + return false } - recipientType.current = type - return false } + recipientType.current = type + return isValid + } catch (error) { + console.error('Error while validating recipient input field:', error) + Senty.captureException(error) + return false } - recipientType.current = type - return isValid - } catch (error) { - console.error('Error while validating recipient input field:', error) - Senty.captureException(error) - return false - } - }, []) + }, + [isWithdrawal] + ) const onInputUpdate = useCallback( (update: InputUpdate) => { diff --git a/src/components/Global/GuestLoginModal/index.tsx b/src/components/Global/GuestLoginModal/index.tsx index b7b7ef052..ff900030a 100644 --- a/src/components/Global/GuestLoginModal/index.tsx +++ b/src/components/Global/GuestLoginModal/index.tsx @@ -1,23 +1,21 @@ -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { useToast } from '@/components/0_Bruddle/Toast' import Modal from '@/components/Global/Modal' import { useZeroDev } from '@/hooks/useZeroDev' import Link from 'next/link' -import { useAppDispatch, useWalletStore } from '@/redux/hooks' -import { walletActions } from '@/redux/slices/wallet-slice' +import { useModalsContext } from '@/context/ModalsContext' const GuestLoginModal = () => { - const dispatch = useAppDispatch() - const { signInModalVisible } = useWalletStore() + const { isSignInModalOpen, setIsSignInModalOpen } = useModalsContext() const { handleLogin, isLoggingIn } = useZeroDev() const toast = useToast() const closeModal = () => { - dispatch(walletActions.setSignInModalVisible(false)) + setIsSignInModalOpen(false) } return ( - +
diff --git a/src/components/Global/InfoRow/index.tsx b/src/components/Global/InfoRow/index.tsx deleted file mode 100644 index 818b8aead..000000000 --- a/src/components/Global/InfoRow/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { twMerge } from 'tailwind-merge' -import Icon from '../Icon' -import MoreInfo from '../MoreInfo' - -interface InfoRowProps { - iconName: string - label: string - value: number | string - moreInfoText: string - loading?: boolean - smallFont?: boolean -} - -const InfoRow = ({ iconName, label, value, moreInfoText, loading, smallFont }: InfoRowProps) => ( -
-
- - -
- {loading ? ( -
- ) : ( -
-
- {value} -
- -
- )} -
-) - -export default InfoRow diff --git a/src/components/Global/InvitesGraph/index.tsx b/src/components/Global/InvitesGraph/index.tsx index 6340e9b46..32f2ff19e 100644 --- a/src/components/Global/InvitesGraph/index.tsx +++ b/src/components/Global/InvitesGraph/index.tsx @@ -10,6 +10,10 @@ const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { ssr: false, }) as any +// Constants for drag vs click detection +const CLICK_MAX_DURATION_MS = 200 +const CLICK_MAX_DISTANCE_PX = 5 + // Types export interface GraphNode { id: string @@ -255,8 +259,13 @@ export default function InvitesGraph(props: InvitesGraphProps) { const graphRef = useRef(null) const containerRef = useRef(null) const forcesConfiguredRef = useRef(false) + const initialZoomDoneRef = useRef(false) const [containerWidth, setContainerWidth] = useState(null) + // Drag vs click detection + const dragStartRef = useRef<{ x: number; y: number; time: number } | null>(null) + const isDraggingRef = useRef(false) + // Measure container width for minimal mode useEffect(() => { if (!isMinimal || !containerRef.current) return @@ -359,7 +368,41 @@ export default function InvitesGraph(props: InvitesGraphProps) { return link.type === 'DIRECT' ? 1.5 : 1 }, []) + // Handle drag start to track for click vs drag detection + const handleNodeDragStart = useCallback((node: any, _translate: any) => { + dragStartRef.current = { x: node.x, y: node.y, time: Date.now() } + isDraggingRef.current = false + }, []) + + // Handle drag to detect actual dragging + const handleNodeDrag = useCallback((node: any) => { + if (!dragStartRef.current) return + const dx = node.x - dragStartRef.current.x + const dy = node.y - dragStartRef.current.y + const distance = Math.sqrt(dx * dx + dy * dy) + if (distance > CLICK_MAX_DISTANCE_PX) { + isDraggingRef.current = true + } + }, []) + + // Handle drag end + const handleNodeDragEnd = useCallback(() => { + // Small delay to let the click handler check the drag state + setTimeout(() => { + dragStartRef.current = null + isDraggingRef.current = false + }, 50) + }, []) + const handleNodeClick = useCallback((node: any) => { + // Skip click if we were dragging + if (isDraggingRef.current) { + return + } + // Also check time-based threshold + if (dragStartRef.current && Date.now() - dragStartRef.current.time > CLICK_MAX_DURATION_MS) { + return + } setSelectedUserId((prev) => (prev === node.id ? null : node.id)) }, []) @@ -393,15 +436,20 @@ export default function InvitesGraph(props: InvitesGraphProps) { setSearchResults([]) }, []) - // Configure D3 forces + // Configure D3 forces - called as soon as graph ref is available const configureForces = useCallback(async () => { if (!graphRef.current || forcesConfiguredRef.current) return const graph = graphRef.current + const nodeCount = filteredGraphData?.nodes?.length ?? 0 + + // Scale force parameters based on node count + const chargeStrength = nodeCount > 50 ? -300 : nodeCount > 20 ? -200 : -150 + const linkDistance = nodeCount > 50 ? 100 : nodeCount > 20 ? 80 : 60 - graph.d3Force('charge')?.strength(-150) - graph.d3Force('charge')?.distanceMax(300) - graph.d3Force('link')?.distance(60) + graph.d3Force('charge')?.strength(chargeStrength) + graph.d3Force('charge')?.distanceMax(400) + graph.d3Force('link')?.distance(linkDistance) graph.d3Force('link')?.strength(0.7) const d3 = await import('d3-force') @@ -412,15 +460,56 @@ export default function InvitesGraph(props: InvitesGraphProps) { const hasAccess = node.hasAppAccess const baseSize = hasAccess ? 6 : 3 const pointsMultiplier = Math.sqrt(node.totalPoints) / 10 - return baseSize + Math.min(pointsMultiplier, 25) + 15 + return baseSize + Math.min(pointsMultiplier, 25) + 20 }) - .strength(0.9) + .strength(1) ) graph.d3Force('center', d3.forceCenter()) forcesConfiguredRef.current = true + + // Reheat the simulation to apply forces properly + graph.d3ReheatSimulation() + }, [filteredGraphData?.nodes?.length]) + + // Initial zoom to fit after graph stabilizes + const handleEngineStop = useCallback(() => { + if (!graphRef.current || initialZoomDoneRef.current) return + // Zoom to fit with padding after initial simulation + setTimeout(() => { + graphRef.current?.zoomToFit(400, 40) + initialZoomDoneRef.current = true + }, 100) }, []) + // Configure forces early - poll until graphRef is available + useEffect(() => { + if (forcesConfiguredRef.current) return + + const checkAndConfigure = () => { + if (graphRef.current && !forcesConfiguredRef.current) { + configureForces() + } + } + + // Try immediately + checkAndConfigure() + + // Also poll a few times in case ref isn't ready yet + const intervals = [50, 100, 200, 500].map((delay) => setTimeout(checkAndConfigure, delay)) + + return () => intervals.forEach(clearTimeout) + }, [configureForces, filteredGraphData]) + + // Reset force config and zoom when filtered data changes (e.g., user selected) + useEffect(() => { + if (selectedUserId !== null) { + // Reset zoom done flag when viewing a subset + initialZoomDoneRef.current = false + forcesConfiguredRef.current = false + } + }, [selectedUserId]) + // Center on selected node useEffect(() => { if (selectedUserId && graphRef.current && filteredGraphData) { @@ -479,7 +568,7 @@ export default function InvitesGraph(props: InvitesGraphProps) { if (isMinimal) { const minimalWidth = containerWidth ?? width ?? 350 return ( -
+
{containerWidth !== null && ( )} + {/* Back button when viewing filtered tree */} + {selectedUserId && ( + + )} {renderOverlays?.({ showUsernames, setShowUsernames, showPoints, setShowPoints })}
) @@ -653,7 +757,7 @@ export default function InvitesGraph(props: InvitesGraphProps) {
{/* Graph Canvas */} -
+
+
{Array.isArray(message) ? message.map((msg, index) => ( @@ -71,6 +70,6 @@ export function MarqueeComp({
)} - +
) } diff --git a/src/components/Global/Modal/index.tsx b/src/components/Global/Modal/index.tsx index 6a7f3f536..03184501f 100644 --- a/src/components/Global/Modal/index.tsx +++ b/src/components/Global/Modal/index.tsx @@ -1,7 +1,7 @@ -import Icon from '@/components/Global/Icon' import { Dialog, Transition } from '@headlessui/react' import { Fragment, useRef } from 'react' import { twMerge } from 'tailwind-merge' +import { Icon } from '../Icons/Icon' type ModalProps = { className?: string @@ -16,8 +16,6 @@ type ModalProps = { video?: boolean hideOverlay?: boolean classNameWrapperDiv?: string - showPrev?: boolean - onPrev?: () => void preventClose?: boolean } @@ -34,8 +32,6 @@ const Modal = ({ video, hideOverlay, classNameWrapperDiv, - showPrev, - onPrev, preventClose = false, }: ModalProps) => { let dialogRef = useRef(null) @@ -86,20 +82,12 @@ const Modal = ({ > {!hideOverlay ? ( <> - {showPrev && ( - - )} {title ? ( <>
{title}
@@ -117,7 +105,7 @@ const Modal = ({ )} onClick={onClose} > - + ) : ( diff --git a/src/components/Global/MoreInfo/index.tsx b/src/components/Global/MoreInfo/index.tsx index 354682c90..e90ceea28 100644 --- a/src/components/Global/MoreInfo/index.tsx +++ b/src/components/Global/MoreInfo/index.tsx @@ -1,7 +1,7 @@ import { Menu, Transition } from '@headlessui/react' import { useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' -import Icon from '../Icon' +import { Icon } from '../Icons/Icon' interface MoreInfoProps { text: string | React.ReactNode diff --git a/src/components/Global/NavHeader/index.tsx b/src/components/Global/NavHeader/index.tsx index 01026f708..976f58d18 100644 --- a/src/components/Global/NavHeader/index.tsx +++ b/src/components/Global/NavHeader/index.tsx @@ -1,5 +1,5 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import Link from 'next/link' import { twMerge } from 'tailwind-merge' import { Icon, type IconName } from '../Icons/Icon' diff --git a/src/components/Global/NoMoreJailModal/index.tsx b/src/components/Global/NoMoreJailModal/index.tsx index 2373f53a0..4fb3c6650 100644 --- a/src/components/Global/NoMoreJailModal/index.tsx +++ b/src/components/Global/NoMoreJailModal/index.tsx @@ -1,9 +1,9 @@ 'use client' -import React, { type FC, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import Image from 'next/image' import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets' import Modal from '../Modal' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import chillPeanutAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_01.gif' const NoMoreJailModal = () => { diff --git a/src/components/Global/OfflineScreen/index.tsx b/src/components/Global/OfflineScreen/index.tsx index 6efbc5a10..e9e9823a0 100644 --- a/src/components/Global/OfflineScreen/index.tsx +++ b/src/components/Global/OfflineScreen/index.tsx @@ -1,6 +1,6 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' // inline peanut icon svg to ensure it works offline without needing to fetch external assets const PeanutIcon = ({ className }: { className?: string }) => ( diff --git a/src/components/Global/PaymentsFooter/index.tsx b/src/components/Global/PaymentsFooter/index.tsx deleted file mode 100644 index f80704c60..000000000 --- a/src/components/Global/PaymentsFooter/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import Icon from '@/components/Global/Icon' -import Link from 'next/link' -import { twMerge } from 'tailwind-merge' - -interface PaymentsFooterProps { - href?: string - text?: string - icon?: string - className?: HTMLDivElement['className'] -} - -export const PaymentsFooter = ({ - href = '/history', - text = 'See your payments', - icon = 'profile', - className, -}: PaymentsFooterProps) => { - return ( - -
- -
- {text} - - ) -} diff --git a/src/components/Global/PeanutActionCard/index.tsx b/src/components/Global/PeanutActionCard/index.tsx index db74adff6..489681dc6 100644 --- a/src/components/Global/PeanutActionCard/index.tsx +++ b/src/components/Global/PeanutActionCard/index.tsx @@ -24,7 +24,7 @@ const PeanutActionCard = ({ type }: PeanutActionCardProps) => {
Socials -

Perfect for group chats!

+

Perfect to DM friends!

diff --git a/src/components/Global/PeanutActionDetailsCard/index.tsx b/src/components/Global/PeanutActionDetailsCard/index.tsx index df52aa665..0054d0661 100644 --- a/src/components/Global/PeanutActionDetailsCard/index.tsx +++ b/src/components/Global/PeanutActionDetailsCard/index.tsx @@ -1,7 +1,6 @@ import AvatarWithBadge, { type AvatarSize } from '@/components/Profile/AvatarWithBadge' -import { PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants' import { type RecipientType } from '@/lib/url-parser/types/payment' -import { printableAddress } from '@/utils' +import { printableAddress } from '@/utils/general.utils' import { AVATAR_TEXT_DARK, getColorForUsername } from '@/utils/color.utils' import { useCallback } from 'react' import { twMerge } from 'tailwind-merge' @@ -11,6 +10,7 @@ import { Icon, type IconName } from '../Icons/Icon' import RouteExpiryTimer from '../RouteExpiryTimer' import Image, { type StaticImageData } from 'next/image' import Loading from '../Loading' +import { PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants/zerodev.consts' export type PeanutActionDetailsCardTransactionType = | 'REQUEST' @@ -115,7 +115,7 @@ export default function PeanutActionDetailsCard({ if (transactionType === 'REGIONAL_METHOD_CLAIM') title = recipientName // Render the string as is for regional method return (

- {icon && } {title} + {icon && } {title}

) } @@ -183,7 +183,7 @@ export default function PeanutActionDetailsCard({ )} {!isRegionalMethodClaim && (
- +
)}
diff --git a/src/components/Global/PeanutSponsored/index.tsx b/src/components/Global/PeanutSponsored/index.tsx deleted file mode 100644 index bda90915f..000000000 --- a/src/components/Global/PeanutSponsored/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import Icon from '../Icon' - -const PeanutSponsored = () => { - return ( -
- - -
- ) -} - -export default PeanutSponsored diff --git a/src/components/Global/PostSignupActionManager/index.tsx b/src/components/Global/PostSignupActionManager/index.tsx index 9fcb060bd..4c0821810 100644 --- a/src/components/Global/PostSignupActionManager/index.tsx +++ b/src/components/Global/PostSignupActionManager/index.tsx @@ -1,6 +1,6 @@ 'use client' -import { getRedirectUrl, clearRedirectUrl } from '@/utils' +import { getRedirectUrl, clearRedirectUrl } from '@/utils/general.utils' import { useRouter } from 'next/navigation' import { useEffect, useState } from 'react' import ActionModal from '../ActionModal' diff --git a/src/components/Global/QRScanner/index.tsx b/src/components/Global/QRScanner/index.tsx index d41c73113..173492f24 100644 --- a/src/components/Global/QRScanner/index.tsx +++ b/src/components/Global/QRScanner/index.tsx @@ -1,14 +1,14 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { useToast } from '@/components/0_Bruddle/Toast' -import { Button } from '@/components/0_Bruddle' -import Icon from '@/components/Global/Icon' +import { Button } from '@/components/0_Bruddle/Button' import { createPortal } from 'react-dom' import jsQR from 'jsqr' import { useDeviceType, DeviceType } from '@/hooks/useGetDeviceType' -import { MERCADO_PAGO, PIX, SIMPLEFI } from '@/assets/payment-apps' +import { MERCADO_PAGO, PIX } from '@/assets/payment-apps' import { PEANUTMAN_LOGO } from '@/assets/peanut' import { ETHEREUM_ICON } from '@/assets/icons' import Image from 'next/image' +import { Icon } from '../Icons/Icon' // QR Scanner Configuration const QR_SCAN_INTERVAL_MS = 100 // Scan every 100ms (10 times per second) @@ -85,28 +85,24 @@ export default function QRScanner({ onScan, onClose, isOpen = true }: QRScannerP console.error('Error closing QR scanner:', error) } }, [onClose, stopCamera]) - const handleQRScan = useCallback( - async (data: string) => { - if (processingQR) return - try { - setProcessingQR(true) - const result = await onScan(data) - if (result.success) { - toast.info('QR code recognized') - } else { - toast.error(result.error || 'QR code processing failed') - // Resume scanning - setProcessingQR(false) - } - } catch (error) { - console.error('Error processing QR code:', error) - toast.error('Error processing QR code') + const handleQRScan = async (data: string) => { + try { + setProcessingQR(true) + const result = await onScan(data) + if (result.success) { + toast.info('QR code recognized') + } else { + toast.error(result.error || 'QR code processing failed') + // Resume scanning setProcessingQR(false) } - }, - [processingQR] - ) - const setupQRScanning = useCallback(() => { + } catch (error) { + console.error('Error processing QR code:', error) + toast.error('Error processing QR code') + setProcessingQR(false) + } + } + const setupQRScanning = () => { if (!videoRef.current || !canvasRef.current) return const video = videoRef.current const canvas = canvasRef.current @@ -132,7 +128,7 @@ export default function QRScanner({ onScan, onClose, isOpen = true }: QRScannerP inversionAttempts: 'attemptBoth', }) - if (code && !processingQR) { + if (code) { handleQRScan(code.data) } } catch (err) { @@ -140,7 +136,7 @@ export default function QRScanner({ onScan, onClose, isOpen = true }: QRScannerP } } }, QR_SCAN_INTERVAL_MS) - }, [handleQRScan, processingQR]) + } const startCamera = useCallback(async () => { setError(null) if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { @@ -214,7 +210,7 @@ export default function QRScanner({ onScan, onClose, isOpen = true }: QRScannerP } else { setError('Your browser does not support camera access') } - }, [facingMode, setupQRScanning, isScanning, deviceType]) + }, [facingMode, isScanning, deviceType]) // Handle visibility change - pause camera when app goes to background useEffect(() => { const handleVisibilityChange = () => { @@ -297,7 +293,7 @@ export default function QRScanner({ onScan, onClose, isOpen = true }: QRScannerP className="border-1 mx-auto flex h-8 w-8 items-center justify-center border-white p-0" onClick={closeScanner} > - + Scan to pay
diff --git a/src/components/Global/RecipientInput/index.tsx b/src/components/Global/RecipientInput/index.tsx deleted file mode 100644 index 946c9cfa6..000000000 --- a/src/components/Global/RecipientInput/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -'use client' - -import Icon from '@/components/Global/Icon' - -type RecipientInputProps = { - placeholder: string - value: string - setValue: (value: string) => void - onEnter?: () => void -} - -const RecipientInput = ({ placeholder, value, setValue, onEnter }: RecipientInputProps) => { - return ( -
- setValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' && onEnter) { - onEnter() - } - }} - /> - {value && ( -
- -
- )} -
- ) -} - -export default RecipientInput diff --git a/src/components/Global/RewardsModal/index.tsx b/src/components/Global/RewardsModal/index.tsx deleted file mode 100644 index 9df918f6a..000000000 --- a/src/components/Global/RewardsModal/index.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { PEANUTMAN_RAISING_HANDS } from '@/assets' -import { Button } from '@/components/0_Bruddle' -import { useAuth } from '@/context/authContext' -import { rewardsApi } from '@/services/rewards' -import { type RewardLink } from '@/services/services.types' -import { hitUserMetric } from '@/utils/metrics.utils' -import Image from 'next/image' -import Link from 'next/link' -import { useRouter } from 'next/navigation' -import { useEffect, useState } from 'react' -import Icon from '../Icon' -import Modal from '../Modal' - -enum REWARD_ASSET_TYPE { - 'PNT' = 'stable_day_berlin_blockchain_week_2025_1_pnt', - 'USDC' = 'aleph_pinta_mar_2025_welcome_usdc', - 'DEPOSIT' = 'aleph_pinta_mar_2025_deposit_reward_pnt', -} - -export const RewardDetails = () => { - return ( -
-
- -
Exclusive benefits:
-
-
    -
  • Visit our booth to unlock more rewards
  • -
-
- ) -} - -export const PartnerBarLocation = ({ text = 'partner bars.' }: { text?: string }) => { - return ( - - {text} - - ) -} - -const RewardsModal = () => { - const [showModal, setShowModal] = useState(false) - const [rewardLinks, setRewardLinks] = useState([]) - const [activeReward, setActiveReward] = useState(undefined) - const { user } = useAuth() - const router = useRouter() - - // get active reward link (pnt first, then usdc) - const getActiveReward = () => { - return rewardLinks.find((r) => r.assetCode === REWARD_ASSET_TYPE.PNT) - /* - const pntReward = rewardLinks.find((r) => r.assetCode === REWARD_ASSET_TYPE.PNT) - const usdcReward = rewardLinks.find((r) => r.assetCode === REWARD_ASSET_TYPE.USDC) - return pntReward || usdcReward - */ - } - - useEffect(() => { - const activeReward = getActiveReward() - setActiveReward(activeReward) - }, [rewardLinks]) - - // get modal content based on active reward - const getModalContent = () => { - //TODO: change after aleph event - return { - title: 'Welcome to Peanut!', - subtitle: ( - - Here's a Beer to celebrate your new{' '} - Peanut Wallet - - ), - ctaText: 'Claim your Beer!', - } - /* - if (!activeReward) return null - - const isPNTReward = activeReward.assetCode === REWARD_ASSET_TYPE.PNT - - return { - title: isPNTReward ? 'Welcome to Peanut!' : `Wait, there's more!`, - subtitle: isPNTReward ? ( - "You've received 2 Beers!" - ) : ( - - Here's $5 for you to explore{' '} - Peanut Wallet and its features! - - ), - ctaText: isPNTReward ? 'Claim your Beers!' : 'Claim', - } - */ - } - - useEffect(() => { - if (user) { - rewardsApi - .getByUser(user.user.userId) - .then((res) => { - setRewardLinks(res) - }) - .catch((_err) => { - console.log('rewards api err', _err) - }) - } - }, [user]) - - useEffect(() => { - if (!rewardLinks.length || !activeReward) { - setShowModal(false) - } else { - setShowModal(true) - } - }, [rewardLinks, activeReward]) - - const modalContent = getModalContent() - - return ( -
- setShowModal(false)} - className="items-center rounded-none" - classWrap="sm:m-auto sm:self-center self-center m-4 bg-background rounded-none border-0" - > - {/* Main content container */} -
-
-
-
-

{modalContent?.title}

-
{modalContent?.subtitle}
-
- {activeReward?.assetCode === REWARD_ASSET_TYPE.PNT ? ( -

- During Stablecoin Day at Berlin Blockchain Week, use your Beer Tokens to enjoy free - beers at the Peanut booth -

- ) : ( -

Your seamless crypto experience starts now.

- )} -
- {activeReward?.assetCode === REWARD_ASSET_TYPE.PNT && } - - -
-
- - {/* Peanutman with beer character at the top */} -
-
- Peanut Man -
-
-
-
- ) -} - -export default RewardsModal diff --git a/src/components/Global/Search/index.tsx b/src/components/Global/Search/index.tsx deleted file mode 100644 index 8a370f253..000000000 --- a/src/components/Global/Search/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import Icon from '@/components/Global/Icon' - -type SearchProps = { - className?: string - placeholder: string - value: string - onChange: any - onSubmit: any - large?: boolean - medium?: boolean - border?: boolean -} - -const Search = ({ className, placeholder, value, onChange, onSubmit, large, medium, border }: SearchProps) => { - return ( -
- - {' '} -
- ) -} - -export default Search diff --git a/src/components/Global/Select/index.tsx b/src/components/Global/Select/index.tsx index 3ba6c0fb4..198798637 100644 --- a/src/components/Global/Select/index.tsx +++ b/src/components/Global/Select/index.tsx @@ -1,8 +1,8 @@ -import Icon from '@/components/Global/Icon' import { Listbox, Transition } from '@headlessui/react' import { useRef } from 'react' import { createPortal } from 'react-dom' import { twMerge } from 'tailwind-merge' +import { Icon } from '../Icons/Icon' type SelectItem = { id: string @@ -66,7 +66,7 @@ const Select = ({ small ? '-mr-2 ml-2' : '' } ${open ? 'rotate-180' : ''} ${classArrow}` )} - name="arrow-bottom" + name="chevron-down" /> {open && diff --git a/src/components/Global/ShareButton/index.tsx b/src/components/Global/ShareButton/index.tsx index 397df771e..5df9ce5c4 100644 --- a/src/components/Global/ShareButton/index.tsx +++ b/src/components/Global/ShareButton/index.tsx @@ -1,10 +1,10 @@ 'use client' -import { Button, type ButtonVariant } from '@/components/0_Bruddle' import { useToast } from '@/components/0_Bruddle/Toast' import * as Sentry from '@sentry/nextjs' import { useCallback } from 'react' import { Icon } from '../Icons/Icon' +import { Button, type ButtonVariant } from '@/components/0_Bruddle/Button' type ShareButtonProps = { title?: string diff --git a/src/components/Global/Sorting/index.tsx b/src/components/Global/Sorting/index.tsx deleted file mode 100644 index 1fbd91bde..000000000 --- a/src/components/Global/Sorting/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useState } from 'react' -import Icon from '@/components/Global/Icon' - -type SortingProps = { - title: string -} - -const Sorting = ({ title }: SortingProps) => { - const [active, setActive] = useState(false) - return ( - - ) -} - -export default Sorting diff --git a/src/components/Global/StatusPill/index.tsx b/src/components/Global/StatusPill/index.tsx index 89a986a07..269888fe7 100644 --- a/src/components/Global/StatusPill/index.tsx +++ b/src/components/Global/StatusPill/index.tsx @@ -13,6 +13,7 @@ const StatusPill = ({ status }: StatusPillProps) => { completed: 'border-success-5 bg-success-2 text-success-4', pending: 'border-yellow-8 bg-secondary-4 text-yellow-6', cancelled: 'border-error-2 bg-error-1 text-error', + refunded: 'border-error-2 bg-error-1 text-error', failed: 'border-error-2 bg-error-1 text-error', processing: 'border-yellow-8 bg-secondary-4 text-yellow-6', soon: 'border-yellow-8 bg-secondary-4 text-yellow-6', @@ -26,6 +27,7 @@ const StatusPill = ({ status }: StatusPillProps) => { soon: 'pending', pending: 'pending', cancelled: 'cancel', + refunded: 'cancel', closed: 'success', } @@ -36,6 +38,7 @@ const StatusPill = ({ status }: StatusPillProps) => { soon: 7, pending: 8, cancelled: 6, + refunded: 6, closed: 7, } diff --git a/src/components/Global/StatusViewWrapper/index.tsx b/src/components/Global/StatusViewWrapper/index.tsx deleted file mode 100644 index 156506fde..000000000 --- a/src/components/Global/StatusViewWrapper/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Card } from '@/components/0_Bruddle' -import { CrispButton } from '@/components/CrispChat' -import React from 'react' -import { PaymentsFooter } from '../PaymentsFooter' - -interface StatusViewWrapperProps { - title: React.ReactNode - description?: string - children?: React.ReactNode - hideSupportCta?: boolean - supportCtaText?: string -} - -const StatusViewWrapper: React.FC = ({ - title, - description, - children, - hideSupportCta = false, - supportCtaText, -}) => { - return ( - - - {title} - {description && {description}} - - {children && ( - -
{children}
-
- )} - {!hideSupportCta && ( - - )} -
- -
-
- ) -} - -export default StatusViewWrapper diff --git a/src/components/Global/SupportDrawer/index.tsx b/src/components/Global/SupportDrawer/index.tsx index 68611fb17..74e2eb5a7 100644 --- a/src/components/Global/SupportDrawer/index.tsx +++ b/src/components/Global/SupportDrawer/index.tsx @@ -1,14 +1,14 @@ 'use client' import { useState, useEffect } from 'react' -import { useSupportModalContext } from '@/context/SupportModalContext' +import { useModalsContext } from '@/context/ModalsContext' import { useCrispUserData } from '@/hooks/useCrispUserData' import { useCrispProxyUrl } from '@/hooks/useCrispProxyUrl' import { Drawer, DrawerContent, DrawerTitle } from '../Drawer' import PeanutLoading from '../PeanutLoading' const SupportDrawer = () => { - const { isSupportModalOpen, setIsSupportModalOpen, prefilledMessage } = useSupportModalContext() + const { isSupportModalOpen, setIsSupportModalOpen, supportPrefilledMessage: prefilledMessage } = useModalsContext() const userData = useCrispUserData() const [isLoading, setIsLoading] = useState(true) diff --git a/src/components/Global/TablePagination/index.tsx b/src/components/Global/TablePagination/index.tsx deleted file mode 100644 index 04dc1fe94..000000000 --- a/src/components/Global/TablePagination/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -'use client' -import Icon from '@/components/Global/Icon' - -type TablePaginationProps = { - onNext: () => void - onPrev: () => void - currentPage: number - totalPages: number -} - -const TablePagination = ({ onNext, onPrev, currentPage, totalPages }: TablePaginationProps) => ( -
- -
- Page {currentPage} of {totalPages} -
- -
-) - -export default TablePagination diff --git a/src/components/Global/Testimonials/index.tsx b/src/components/Global/Testimonials/index.tsx index a421d5277..6f05207a7 100644 --- a/src/components/Global/Testimonials/index.tsx +++ b/src/components/Global/Testimonials/index.tsx @@ -1,7 +1,6 @@ import { useRef } from 'react' -import { Stack, Box, Flex, SimpleGrid, GridItem } from '@chakra-ui/react' import { motion } from 'framer-motion' -import { useMediaQuery } from '@chakra-ui/react' +import useMediaQuery from '@mui/material/useMediaQuery' interface Testimonial { name: string @@ -18,9 +17,9 @@ type TestimonialsProps = { export function Testimonials({ testimonials }: TestimonialsProps) { const ref = useRef(null) - const [isLargerThan768] = useMediaQuery('(min-width: 768px)') + const isLargerThan768 = useMediaQuery('(min-width: 768px)') - // Animation variants + // animation variants const gridItemVariants = [ { hidden: { opacity: 0, translateY: 20, translateX: 0, rotate: 0 }, @@ -164,10 +163,10 @@ export function Testimonials({ testimonials }: TestimonialsProps) { ] return ( -
- +
+
{testimonials.map((testimonial, index) => ( - +
{isLargerThan768 ? (
)} -
+
))} - +
) } diff --git a/src/components/Global/TextEdit/index.tsx b/src/components/Global/TextEdit/index.tsx deleted file mode 100644 index ddda91e9c..000000000 --- a/src/components/Global/TextEdit/index.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react' - -const TextEdit = ({ initialText, onTextChange }: { initialText: string; onTextChange: (text: string) => void }) => { - const [text, setText] = useState(initialText) - const [isEditing, setIsEditing] = useState(false) - const inputRef = useRef(null) - - useEffect(() => { - if (isEditing && inputRef.current) { - inputRef.current.focus() - } - }, [isEditing]) - - const handleTextChange = (event: React.ChangeEvent) => { - setText(event.target.value) - } - - const handleBlur = () => { - setIsEditing(false) - onTextChange(text) - } - - const handleKeyDown = (event: React.KeyboardEvent) => { - const key = event.key - - // Allow alphanumeric characters and control keys - if (!/^[a-zA-Z0-9]$/.test(key) && !['Backspace', 'ArrowLeft', 'ArrowRight', 'Delete'].includes(key)) { - event.preventDefault() - } - if (event.key === 'Enter') { - setIsEditing(false) - onTextChange(text) - } - } - - useEffect(() => { - setText(initialText) - }, [initialText]) - - return ( -
- {isEditing ? ( - - ) : ( - { - setIsEditing(true) - }} - > - {text} - - )} - - {!isEditing && ( - - )} -
- ) -} - -export default TextEdit diff --git a/src/components/Global/Timeline/index.tsx b/src/components/Global/Timeline/index.tsx deleted file mode 100644 index 84da01f3e..000000000 --- a/src/components/Global/Timeline/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -interface TimelineProps { - label: string - value: string -} - -const Timeline = ({ label, value }: TimelineProps) => { - return ( -
-
-
-
-
{label}
-
{value}
-
-
- ) -} - -export default Timeline diff --git a/src/components/Global/ToggleTheme/index.tsx b/src/components/Global/ToggleTheme/index.tsx deleted file mode 100644 index e8503dafd..000000000 --- a/src/components/Global/ToggleTheme/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client' -import Icon from '@/components/Global/Icon' -import { useColorMode } from '@chakra-ui/color-mode' - -type ToggleThemeProps = {} - -const ToggleTheme = ({}: ToggleThemeProps) => { - const { colorMode, setColorMode } = useColorMode() - - const items: { - icon: string - active: boolean - onClick: () => void - }[] = [ - { - icon: 'sun', - active: colorMode === 'light', - onClick: () => setColorMode('light'), - }, - { - icon: 'moon', - active: colorMode === 'dark', - onClick: () => setColorMode('dark'), - }, - ] - - return ( -
- {items.map((item, index) => ( - - ))} -
- ) -} - -export default ToggleTheme diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx deleted file mode 100644 index ac6feaaf9..000000000 --- a/src/components/Global/TokenAmountInput/index.tsx +++ /dev/null @@ -1,416 +0,0 @@ -'use client' - -import { PEANUT_WALLET_TOKEN_DECIMALS, STABLE_COINS } from '@/constants' -import { tokenSelectorContext } from '@/context' -import { formatTokenAmount, formatCurrency } from '@/utils' -import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' -import { useRouter, useSearchParams } from 'next/navigation' -import Icon from '../Icon' -import { twMerge } from 'tailwind-merge' -import { Icon as IconComponent } from '@/components/Global/Icons/Icon' -import { Slider } from '../Slider' -import { DeviceType, useDeviceType } from '@/hooks/useGetDeviceType' - -interface TokenAmountInputProps { - className?: string - tokenValue: string | undefined - setTokenValue: (tokenvalue: string | undefined) => void - setUsdValue?: (usdvalue: string) => void - setCurrencyAmount?: (currencyvalue: string | undefined) => void - onSubmit?: () => void - onBlur?: () => void - disabled?: boolean - walletBalance?: string - currency?: { - code: string - symbol: string - price: number - } - hideCurrencyToggle?: boolean - hideBalance?: boolean - showInfoText?: boolean - infoText?: string - showSlider?: boolean - maxAmount?: number - amountCollected?: number - isInitialInputUsd?: boolean - defaultSliderValue?: number - defaultSliderSuggestedAmount?: number -} - -const TokenAmountInput = ({ - className, - tokenValue, - setTokenValue, - setCurrencyAmount, - onSubmit, - onBlur, - disabled, - walletBalance, - currency, - setUsdValue, - hideCurrencyToggle = false, - hideBalance = false, - infoText, - showInfoText, - showSlider = false, - maxAmount, - amountCollected = 0, - isInitialInputUsd = false, - defaultSliderValue, - defaultSliderSuggestedAmount, -}: TokenAmountInputProps) => { - const { selectedTokenData } = useContext(tokenSelectorContext) - const router = useRouter() - const searchParams = useSearchParams() - const inputRef = useRef(null) - const inputType = useMemo(() => (window.innerWidth < 640 ? 'text' : 'number'), []) - const [isFocused, setIsFocused] = useState(false) - const { deviceType } = useDeviceType() - // Only autofocus on desktop (WEB), not on mobile devices (IOS/ANDROID) - const shouldAutoFocus = deviceType === DeviceType.WEB - - // Store display value for input field (what user sees when typing) - const [displayValue, setDisplayValue] = useState(tokenValue || '') - const [isInputUsd, setIsInputUsd] = useState(!currency || isInitialInputUsd) - const [displaySymbol, setDisplaySymbol] = useState('') - const [alternativeDisplayValue, setAlternativeDisplayValue] = useState('0.00') - const [alternativeDisplaySymbol, setAlternativeDisplaySymbol] = useState('') - - const displayMode = useMemo<'TOKEN' | 'STABLE' | 'FIAT'>(() => { - if (currency) return 'FIAT' - if (selectedTokenData?.symbol && STABLE_COINS.includes(selectedTokenData?.symbol)) { - return 'STABLE' - } else { - return 'TOKEN' - } - }, [currency, selectedTokenData?.symbol]) - - const decimals = useMemo(() => { - let _decimals: number - if (displayMode === 'TOKEN' && isInputUsd && selectedTokenData?.decimals) { - _decimals = selectedTokenData.decimals - } else { - _decimals = PEANUT_WALLET_TOKEN_DECIMALS - } - // For displaying the token amount, anything more breaks the UI - return 6 < _decimals ? 6 : _decimals - }, [displayMode, selectedTokenData?.decimals, isInputUsd]) - - const calculateAlternativeValue = useCallback( - (value: string) => { - // There is no alternative display when dealing with stables - if (displayMode === 'STABLE') return '' - - let price: number - if (displayMode === 'TOKEN') { - if (!selectedTokenData?.price) return '' - price = selectedTokenData.price - } else if (displayMode === 'FIAT') { - if (!currency?.price) return '' - price = 1 / currency.price - } else { - throw new Error('Invalid display mode') - } - - if (isInputUsd) { - return formatTokenAmount(Number(value) / price, decimals)! - } else { - return formatTokenAmount(Number(value) * price, decimals)! - } - }, - [displayMode, currency?.price, selectedTokenData?.price, isInputUsd, decimals] - ) - - const onChange = useCallback( - (value: string, _isInputUsd: boolean) => { - setDisplayValue(value) - setAlternativeDisplayValue(calculateAlternativeValue(value)) - let tokenValue: string - switch (displayMode) { - case 'STABLE': { - tokenValue = value - break - } - case 'TOKEN': { - tokenValue = _isInputUsd ? (Number(value) / (selectedTokenData?.price ?? 1)).toString() : value - break - } - case 'FIAT': { - if (!currency?.price) throw new Error('Invalid currency') - const usdValue = _isInputUsd ? Number(value) : Number(value) / currency.price - // For when we have a non stablecoin request in currency mode - tokenValue = formatTokenAmount(usdValue / (selectedTokenData?.price ?? 1), decimals)! - const currencyValue = _isInputUsd ? (Number(value) * currency.price).toString() : value - setCurrencyAmount?.(currencyValue) - break - } - default: { - throw new Error('Invalid display mode') - } - } - setTokenValue(tokenValue) - }, - [displayMode, currency?.price, selectedTokenData?.price, calculateAlternativeValue] - ) - - const onSliderValueChange = useCallback( - (value: number[]) => { - if (maxAmount) { - const selectedPercentage = value[0] - let selectedAmount = (selectedPercentage / 100) * maxAmount - - // Only snap to exact remaining amount when user selects the 33.33% magnetic snap point - // This ensures equal splits fill the pot exactly to 100% - const SNAP_POINT_TOLERANCE = 0.5 // percentage points - allows magnetic snapping - const COMPLETION_THRESHOLD = 0.98 // 98% - if 33.33% would nearly complete pot - const EQUAL_SPLIT_PERCENTAGE = 100 / 3 // 33.333...% - - const isAt33SnapPoint = Math.abs(selectedPercentage - EQUAL_SPLIT_PERCENTAGE) < SNAP_POINT_TOLERANCE - if (isAt33SnapPoint && amountCollected > 0) { - const remainingAmount = maxAmount - amountCollected - // Only snap if there's remaining amount and 33.33% would nearly complete the pot - if (remainingAmount > 0 && selectedAmount >= remainingAmount * COMPLETION_THRESHOLD) { - selectedAmount = remainingAmount - } - } - - const selectedAmountStr = parseFloat(selectedAmount.toFixed(4)).toString() - const maxDecimals = displayMode === 'FIAT' || displayMode === 'STABLE' || isInputUsd ? 2 : decimals - const formattedAmount = formatTokenAmount(selectedAmountStr, maxDecimals, true) - if (formattedAmount) { - onChange(formattedAmount, isInputUsd) - } - } - }, - [maxAmount, amountCollected, onChange, displayMode, isInputUsd, decimals] - ) - - const showConversion = useMemo(() => { - return !hideCurrencyToggle && (displayMode === 'TOKEN' || displayMode === 'FIAT') - }, [hideCurrencyToggle, displayMode]) - - // This is needed because if we change the token we selected the value - // should change. This only depends on the price on purpose!! we don't want - // to change when we change the display mode or the value (we already call - // onchange on the input change so dont add those dependencies here!) - useEffect(() => { - // early return if tokenValue is empty. - if (!tokenValue) return - - if (!isInitialInputUsd) { - const value = tokenValue ? Number(tokenValue) : 0 - const formattedValue = (value * (currency?.price ?? 1)).toFixed(2) - onChange(formattedValue, isInputUsd) - } else { - onChange(displayValue, isInputUsd) - } - }, [selectedTokenData?.price]) - - useEffect(() => { - switch (displayMode) { - case 'STABLE': { - setDisplaySymbol('$') - setAlternativeDisplaySymbol('') - break - } - case 'TOKEN': { - if (isInputUsd) { - setDisplaySymbol('$') - setAlternativeDisplaySymbol(selectedTokenData?.symbol || '') - } else { - setDisplaySymbol(selectedTokenData?.symbol || '') - setAlternativeDisplaySymbol('$') - } - break - } - case 'FIAT': { - if (isInputUsd) { - setDisplaySymbol('USD') - setAlternativeDisplaySymbol(currency?.symbol || '') - } else { - setDisplaySymbol(currency?.symbol || '') - setAlternativeDisplaySymbol('USD') - } - break - } - default: { - throw new Error('Invalid display mode') - } - } - }, [displayMode, selectedTokenData?.symbol, currency?.symbol, isInputUsd]) - - useEffect(() => { - if (inputRef.current) { - if (displayValue?.length !== 0) { - inputRef.current.style.width = `${displayValue?.length ?? 0}ch` - } else { - inputRef.current.style.width = `4ch` - } - } - }, [displayValue, currency]) - - useEffect(() => { - if (!setUsdValue) return - if (displayMode === 'STABLE') { - setUsdValue(displayValue) - } else { - setUsdValue(isInputUsd ? displayValue : alternativeDisplayValue) - } - }, [setUsdValue, displayValue, alternativeDisplayValue, isInputUsd, displayMode]) - - const formRef = useRef(null) - - const sliderValue = useMemo(() => { - if (!maxAmount || !tokenValue) return [0] - const tokenNum = parseFloat(tokenValue.replace(/,/g, '')) - const usdValue = tokenNum * (selectedTokenData?.price ?? 1) - return [(usdValue / maxAmount) * 100] - }, [maxAmount, tokenValue, selectedTokenData?.price]) - - const handleContainerClick = () => { - if (inputRef.current) { - inputRef.current.focus() - } - } - - // Sync default slider suggested amount to the input - useEffect(() => { - if (defaultSliderSuggestedAmount) { - const formattedAmount = formatTokenAmount(defaultSliderSuggestedAmount.toString(), 2) - if (formattedAmount) { - setTokenValue(formattedAmount) - setDisplayValue(formattedAmount) - } - } - }, [defaultSliderSuggestedAmount]) - - return ( -
-
-
- - - {/* Input with fake caret */} -
- { - let value = e.target.value - // USD/currency โ†’ 2 decimals; token input โ†’ allow `decimals` (<= 6) - const maxDecimals = - displayMode === 'FIAT' || displayMode === 'STABLE' || isInputUsd ? 2 : decimals - const formattedAmount = formatTokenAmount(value, maxDecimals, true) - if (formattedAmount !== undefined) { - value = formattedAmount - } - onChange(value, isInputUsd) - }} - ref={inputRef} - inputMode="decimal" - type={inputType} - value={displayValue.replace(/,/g, '')} - step="any" - min="0" - autoComplete="off" - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault() - if (onSubmit) onSubmit() - } - }} - onFocus={() => setIsFocused(true)} - onBlur={() => { - setIsFocused(false) - if (onBlur) onBlur() - }} - disabled={disabled} - /> - {/* Fake blinking caret shown when not focused and input is empty */} - {!isFocused && !displayValue && ( -
- )} -
-
- - {/* Conversion */} - {showConversion && ( - - )} - - {/* Balance */} - {walletBalance && !hideBalance && ( -
- Balance: {displayMode === 'FIAT' && currency ? 'USD ' : '$ '} - {walletBalance} -
- )} -
- {/* Conversion toggle */} - {showConversion && ( -
{ - e.preventDefault() - const currentValue = displayValue - if (!alternativeDisplayValue || alternativeDisplayValue === '0.00') { - setDisplayValue('') - } else { - setDisplayValue(alternativeDisplayValue) - } - if (!currentValue) { - setAlternativeDisplayValue('0.00') - } else { - setAlternativeDisplayValue(currentValue) - } - setIsInputUsd(!isInputUsd) - - // Toggle swap-currency parameter in URL - const params = new URLSearchParams(searchParams.toString()) - const currentSwapValue = params.get('swap-currency') - if (currentSwapValue === 'true') { - params.set('swap-currency', 'false') - } else { - params.set('swap-currency', 'true') - } - router.replace(`?${params.toString()}`, { scroll: false }) - }} - > - -
- )} - {showInfoText && infoText && ( -
- -

{infoText}

-
- )} - {showSlider && maxAmount && ( -
- -
- )} - - ) -} - -export default TokenAmountInput diff --git a/src/components/Global/TokenAndNetworkConfirmationModal/index.tsx b/src/components/Global/TokenAndNetworkConfirmationModal/index.tsx index eb5062de6..ebbab2a59 100644 --- a/src/components/Global/TokenAndNetworkConfirmationModal/index.tsx +++ b/src/components/Global/TokenAndNetworkConfirmationModal/index.tsx @@ -1,26 +1,22 @@ import ActionModal from '@/components/Global/ActionModal' import { Slider } from '@/components/Slider' -import { ARBITRUM_ICON } from '@/assets' -import { type NetworkConfig } from '@/components/Global/TokenSelector/TokenSelector.consts' -import { type CryptoToken } from '@/components/AddMoney/consts' -import Image from 'next/image' +import ChainChip from '@/components/AddMoney/components/ChainChip' +import { + RHINO_SUPPORTED_CHAINS, + RHINO_SUPPORTED_EVM_CHAINS, + RHINO_SUPPORTED_OTHER_CHAINS, + RHINO_SUPPORTED_TOKENS, +} from '@/constants/rhino.consts' export default function TokenAndNetworkConfirmationModal({ - token, - network, onClose, onAccept, isVisible = true, }: { - token?: Pick - network?: Pick onClose: () => void onAccept: () => void isVisible?: boolean }) { - token = token ?? { symbol: 'USDC', icon: 'https://assets.coingecko.com/coins/images/6319/small/USD_Coin_icon.png' } - network = network ?? { name: 'Arbitrum', iconUrl: ARBITRUM_ICON } - return ( -
- {token.symbol} - {token.symbol} -
-
- {network.name} - {network.name} -
- Sending funds via any other network will result in a permanent loss. + Sending the wrong token or using the wrong network will result in permanent loss. + +
+

Supported Networks

+ +
+ {RHINO_SUPPORTED_OTHER_CHAINS.map((chain) => ( + + ))} + {RHINO_SUPPORTED_EVM_CHAINS.slice(0, 6).map((chain) => ( + + ))} + +
+
+ +
+

Supported Tokens

+ +
+ {RHINO_SUPPORTED_TOKENS.map((token) => ( + + ))} +
+
} footer={ @@ -50,7 +66,7 @@ export default function TokenAndNetworkConfirmationModal({
} ctas={[]} - modalPanelClassName="max-w-xs" + modalPanelClassName="max-w-sm" /> ) } diff --git a/src/components/Global/TokenSelector/Components/NetworkButton.tsx b/src/components/Global/TokenSelector/Components/NetworkButton.tsx index a73756bb1..53452b1e2 100644 --- a/src/components/Global/TokenSelector/Components/NetworkButton.tsx +++ b/src/components/Global/TokenSelector/Components/NetworkButton.tsx @@ -1,4 +1,10 @@ -import { Button } from '@/components/0_Bruddle' +/** + * button component for selecting a network in the token selector + * + * displays chain icon and name, or a search icon for the "more" button + */ + +import { Button } from '@/components/0_Bruddle/Button' import AvatarWithBadge from '@/components/Profile/AvatarWithBadge' import Image from 'next/image' import React, { useState } from 'react' diff --git a/src/components/Global/TokenSelector/Components/NetworkListItem.tsx b/src/components/Global/TokenSelector/Components/NetworkListItem.tsx index 177ae3346..9ef4b377d 100644 --- a/src/components/Global/TokenSelector/Components/NetworkListItem.tsx +++ b/src/components/Global/TokenSelector/Components/NetworkListItem.tsx @@ -2,7 +2,7 @@ import Image from 'next/image' import React, { useState } from 'react' import { twMerge } from 'tailwind-merge' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import Card from '@/components/Global/Card' import NavigationArrow from '@/components/Global/NavigationArrow' import AvatarWithBadge from '@/components/Profile/AvatarWithBadge' diff --git a/src/components/Global/TokenSelector/Components/NetworkListView.tsx b/src/components/Global/TokenSelector/Components/NetworkListView.tsx index b1cc74475..2866a4507 100644 --- a/src/components/Global/TokenSelector/Components/NetworkListView.tsx +++ b/src/components/Global/TokenSelector/Components/NetworkListView.tsx @@ -1,3 +1,10 @@ +/** + * full network list view for the token selector + * + * shows searchable list of all supported networks plus "coming soon" networks + * accessed via the "more" button in the popular networks section + */ + import React, { useMemo } from 'react' import EmptyState from '../../EmptyStates/EmptyState' diff --git a/src/components/Global/TokenSelector/Components/SearchInput.tsx b/src/components/Global/TokenSelector/Components/SearchInput.tsx index 62f24691d..9a2a60231 100644 --- a/src/components/Global/TokenSelector/Components/SearchInput.tsx +++ b/src/components/Global/TokenSelector/Components/SearchInput.tsx @@ -1,4 +1,4 @@ -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import BaseInput from '@/components/0_Bruddle/BaseInput' import { Icon } from '@/components/Global/Icons/Icon' import React from 'react' diff --git a/src/components/Global/TokenSelector/Components/TokenListItem.tsx b/src/components/Global/TokenSelector/Components/TokenListItem.tsx index 66315dae6..11fc53132 100644 --- a/src/components/Global/TokenSelector/Components/TokenListItem.tsx +++ b/src/components/Global/TokenSelector/Components/TokenListItem.tsx @@ -1,8 +1,15 @@ +/** + * token list item component for the token selector + * + * displays token icon, symbol, chain, and optionally balance/price + * handles selection state and disabled state for unsupported tokens + */ + import Card, { type CardPosition } from '@/components/Global/Card' import AvatarWithBadge from '@/components/Profile/AvatarWithBadge' import { tokenSelectorContext } from '@/context/tokenSelector.context' import { type IUserBalance } from '@/interfaces' -import { formatAmountWithSignificantDigits, formatAmount } from '@/utils' +import { formatAmountWithSignificantDigits, formatAmount } from '@/utils/general.utils' import Image from 'next/image' import React, { useContext, useMemo, useState } from 'react' import { twMerge } from 'tailwind-merge' diff --git a/src/components/Global/TokenSelector/TokenSelector.tsx b/src/components/Global/TokenSelector/TokenSelector.tsx index 4b9c735a9..04a6d7ba4 100644 --- a/src/components/Global/TokenSelector/TokenSelector.tsx +++ b/src/components/Global/TokenSelector/TokenSelector.tsx @@ -1,18 +1,25 @@ 'use client' +/** + * token and network selector component + * + * allows users to select which token/chain to receive payments on. + * shows popular networks (arb, base, op, eth) and tokens (usdc, usdt, native). + * + * used by: withdraw, claim, and req_pay flows + */ + import Image from 'next/image' -import React, { type ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' +import React, { type ReactNode, useCallback, useContext, useMemo, useRef, useState } from 'react' import { twMerge } from 'tailwind-merge' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import Divider from '@/components/0_Bruddle/Divider' import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants/zerodev.consts' import { tokenSelectorContext } from '@/context' import { type IToken, type IUserBalance } from '@/interfaces' -import { areEvmAddressesEqual, formatTokenAmount, isNativeCurrency, getChainName } from '@/utils' +import { areEvmAddressesEqual, isNativeCurrency, getChainName } from '@/utils/general.utils' import { SQUID_ETH_ADDRESS } from '@/utils/token.utils' -import { initializeAppKit } from '@/config/wagmi.config' -import { useAppKit, useAppKitAccount, useDisconnect } from '@reown/appkit/react' import EmptyState from '../EmptyStates/EmptyState' import { Icon, type IconName } from '../Icons/Icon' import NetworkButton from './Components/NetworkButton' @@ -25,7 +32,6 @@ import { TOKEN_SELECTOR_POPULAR_NETWORK_IDS, TOKEN_SELECTOR_SUPPORTED_NETWORK_IDS, } from './TokenSelector.consts' -import { useWalletBalances } from '@/hooks/useWalletBalances' import { Drawer, DrawerContent, DrawerTitle } from '../Drawer' interface SectionProps { @@ -59,15 +65,6 @@ const TokenSelector: React.FC = ({ classNameButton, viewT const [searchValue, setSearchValue] = useState('') const [showNetworkList, setShowNetworkList] = useState(false) const [networkSearchValue, setNetworkSearchValue] = useState('') - // appkit hooks - const { open: openAppkitModal } = useAppKit() - const { disconnect: disconnectWallet } = useDisconnect() - const { isConnected: isExternalWalletConnected, address: externalWalletAddress } = useAppKitAccount() - - // Fetch external wallet balances using TanStack Query (replaces manual useEffect + refs + state) - const { data: externalBalances = [], isLoading: isLoadingExternalBalances } = useWalletBalances( - isExternalWalletConnected ? externalWalletAddress : undefined - ) // state for image loading errors const [buttonImageError, setButtonImageError] = useState(false) @@ -77,7 +74,6 @@ const TokenSelector: React.FC = ({ classNameButton, viewT setSelectedChainID, selectedTokenAddress, selectedChainID, - setSelectedTokenBalance, } = useContext(tokenSelectorContext) // drawer utility functions @@ -87,45 +83,7 @@ const TokenSelector: React.FC = ({ classNameButton, viewT setTimeout(() => setSearchValue(''), 200) }, []) - // Note: external wallet balance fetching is now handled by useWalletBalances hook - // It automatically: - // - Fetches when wallet connects (enabled guard) - // - Refetches when address changes (queryKey includes address) - // - Clears when wallet disconnects (enabled becomes false) - // - Auto-refreshes every 60 seconds - // No manual refs or state management needed! - - const sourceBalances = useMemo(() => { - if (isExternalWalletConnected) { - return externalBalances // Direct from query (auto-handles all cases) - } else { - return [] // return empty array if wallet not connected - } - }, [isExternalWalletConnected, externalBalances]) - - const tokensOnSelectedNetwork = useMemo(() => { - if (!selectedChainID) { - return sourceBalances // no network selected, return all source balances - } - return sourceBalances.filter((balance) => String(balance.chainId) === selectedChainID) - }, [sourceBalances, selectedChainID, isExternalWalletConnected]) - - // display tokens memo, filters tokensOnSelectedNetwork by search value - const displayUserTokens = useMemo(() => { - const lowerSearchValue = searchValue.toLowerCase() - if (!lowerSearchValue) { - return tokensOnSelectedNetwork // no search value, return all tokens on the network - } - return tokensOnSelectedNetwork.filter((balance) => { - const hasSymbol = !!balance.symbol - const symbolMatch = hasSymbol && balance.symbol.toLowerCase().includes(lowerSearchValue) - const nameMatch = balance.name?.toLowerCase().includes(lowerSearchValue) ?? false - const addressMatch = balance.address?.toLowerCase().includes(lowerSearchValue) ?? false - return hasSymbol && (symbolMatch || nameMatch || addressMatch) - }) - }, [tokensOnSelectedNetwork, searchValue, isExternalWalletConnected]) - - // handles token selection based on token balance + // handles token selection const handleTokenSelect = useCallback( (balance: IUserBalance) => { setSelectedTokenAddress(balance.address) @@ -135,17 +93,6 @@ const TokenSelector: React.FC = ({ classNameButton, viewT [closeDrawer, setSelectedTokenAddress, setSelectedChainID] ) - useEffect(() => { - const tokenBalance = sourceBalances.find( - (balance) => - areEvmAddressesEqual(balance.address, selectedTokenAddress) && - String(balance.chainId) === selectedChainID - ) - if (tokenBalance) { - setSelectedTokenBalance(tokenBalance.amount.toString()) - } - }, [selectedTokenAddress, selectedChainID, sourceBalances]) - // renders network list view const handleSearchNetwork = useCallback(() => { setShowNetworkList(true) @@ -155,14 +102,10 @@ const TokenSelector: React.FC = ({ classNameButton, viewT const handleChainSelectFromList = useCallback( (chainId: string) => { setSelectedChainID(chainId) - if (isExternalWalletConnected) { - setSelectedTokenAddress('') // clear token when chain changes with external wallet - } else { - setSelectedTokenAddress('') // clear selected token when changing network in popular view - } + setSelectedTokenAddress('') // clear selected token when changing network setShowNetworkList(false) }, - [setSelectedChainID, setSelectedTokenAddress, isExternalWalletConnected] + [setSelectedChainID, setSelectedTokenAddress] ) // selected network name memo, being used ui @@ -189,50 +132,20 @@ const TokenSelector: React.FC = ({ classNameButton, viewT } }, [supportedSquidChainsAndTokens]) - // button display variables + // button display variables - derive from selected token/chain let buttonSymbol: string | undefined = undefined let buttonChainName: string | undefined = undefined - let buttonFormattedBalance: string | null = null let buttonLogoURI: string | undefined = undefined let buttonChainLogoURI: string | undefined = peanutWalletTokenDetails?.chainLogoURI - if (isExternalWalletConnected) { - if (selectedTokenAddress && selectedChainID) { - // wallet connected AND token selected - const userBalanceDetails = sourceBalances.find( - (b) => areEvmAddressesEqual(b.address, selectedTokenAddress) && String(b.chainId) === selectedChainID - ) - const chainInfo = supportedSquidChainsAndTokens[selectedChainID] - const generalTokenDetails = chainInfo?.tokens.find((t) => - areEvmAddressesEqual(t.address, selectedTokenAddress) - ) - - if (generalTokenDetails && chainInfo) { - buttonSymbol = generalTokenDetails.symbol - buttonLogoURI = generalTokenDetails.logoURI - buttonChainName = chainInfo.networkName || `Chain ${selectedChainID}` - buttonChainLogoURI = chainInfo.chainIconURI - } - if (userBalanceDetails) { - buttonFormattedBalance = formatTokenAmount(userBalanceDetails.amount) ?? null - } else { - if (generalTokenDetails) buttonFormattedBalance = '0' - } - } - } else { - // no external wallet connected - if (selectedTokenAddress && selectedChainID) { - // popular token selected by user or "usdc on arb" card clicked - const chainInfo = supportedSquidChainsAndTokens[selectedChainID] - const generalTokenDetails = chainInfo?.tokens.find((t) => - areEvmAddressesEqual(t.address, selectedTokenAddress) - ) - if (generalTokenDetails && chainInfo) { - buttonSymbol = generalTokenDetails.symbol - buttonLogoURI = generalTokenDetails.logoURI - buttonChainName = chainInfo.networkName || `Chain ${selectedChainID}` - buttonChainLogoURI = chainInfo.chainIconURI - } + if (selectedTokenAddress && selectedChainID) { + const chainInfo = supportedSquidChainsAndTokens[selectedChainID] + const tokenDetails = chainInfo?.tokens.find((t) => areEvmAddressesEqual(t.address, selectedTokenAddress)) + if (tokenDetails && chainInfo) { + buttonSymbol = tokenDetails.symbol + buttonLogoURI = tokenDetails.logoURI + buttonChainName = chainInfo.networkName || `Chain ${selectedChainID}` + buttonChainLogoURI = chainInfo.chainIconURI } } @@ -253,6 +166,7 @@ const TokenSelector: React.FC = ({ classNameButton, viewT }).filter((chain): chain is { chainId: string; name: string; iconURI: string } => Boolean(chain)) // type guard filter nulls }, [supportedSquidChainsAndTokens]) + // build list of popular tokens (usdc, usdt, native) for display const popularTokensList = useMemo(() => { const popularSymbolsToFind = ['USDC', 'USDT'] const createPopularTokenEntry = (token: IToken, chainId: string): IUserBalance => ({ @@ -325,33 +239,22 @@ const TokenSelector: React.FC = ({ classNameButton, viewT return sortTokensByPriority(uniqueTokens) } - if (!isExternalWalletConnected) { - if (searchValue) { - // search active: show searched token across ALL supported networks - return buildTokensForChainArray(TOKEN_SELECTOR_SUPPORTED_NETWORK_IDS, searchValue) - } - if (selectedChainID) { - // specific chain selected: show popular (USDC, USDT, Native) for that chain - return buildTokensForChainArray([selectedChainID]) - } - // default for no wallet: popular tokens on popular chains - const popularChainIds = popularChainsForButtons.map((pc) => pc.chainId) - return buildTokensForChainArray(popularChainIds) + if (searchValue) { + // search active: show searched token across ALL supported networks + return buildTokensForChainArray(TOKEN_SELECTOR_SUPPORTED_NETWORK_IDS, searchValue) } + if (selectedChainID) { + // specific chain selected: show popular (USDC, USDT, Native) for that chain + return buildTokensForChainArray([selectedChainID]) + } + // default: popular tokens on popular chains + const popularChainIds = popularChainsForButtons.map((pc) => pc.chainId) + return buildTokensForChainArray(popularChainIds) + }, [searchValue, selectedChainID, supportedSquidChainsAndTokens, popularChainsForButtons]) - return [] // no popular tokens if wallet is connected - }, [ - isExternalWalletConnected, - searchValue, - selectedChainID, - supportedSquidChainsAndTokens, - popularChainsForButtons, - ]) - + // filter popular tokens by search const filteredPopularTokensToDisplay = useMemo(() => { - // if searchValue is active, popularTokensList is already filtered by symbol across all chains. - // if not, popularTokensList is for selected/popular chains, and then we apply broad search. - if (!searchValue || isExternalWalletConnected) return popularTokensList // no further filtering if search already did its job or wallet connected + if (!searchValue) return popularTokensList const lowerSearchValue = searchValue.toLowerCase() return popularTokensList.filter((token) => { @@ -361,105 +264,13 @@ const TokenSelector: React.FC = ({ classNameButton, viewT const addressMatch = token.address?.toLowerCase().includes(lowerSearchValue) ?? false return hasSymbol && (symbolMatch || nameMatch || addressMatch) }) - }, [popularTokensList, searchValue, isExternalWalletConnected]) - - // visibility flags - const showPopularTokensList = useMemo(() => !isExternalWalletConnected, [isExternalWalletConnected]) - const showUserTokensList = useMemo(() => isExternalWalletConnected, [isExternalWalletConnected]) - - const renderUserTokenListContent = () => { - if (isLoadingExternalBalances) { - return
Loading balances...
- } - // this section only renders if isExternalWalletConnected is true - if (sourceBalances.length === 0) { - return ( - { - await disconnectWallet() - setTimeout(async () => { - try { - await initializeAppKit() - openAppkitModal() - } catch (error) { - console.error('Failed to initialize AppKit:', error) - } - }, 300) - }} - > - Try connecting to a different wallet. - - } - /> - ) - } - if (selectedChainID && tokensOnSelectedNetwork.length === 0) { - return ( - - You don't have any tokens on {selectedNetworkName} - - } - icon="wallet-cancel" - cta={ - - } - /> - ) - } - if (searchValue && displayUserTokens.length === 0) { - return ( - - ) - } - if (displayUserTokens.length > 0) { - return displayUserTokens.map((balance) => { - const isSelected = - areEvmAddressesEqual(selectedTokenAddress, balance.address) && - selectedChainID === String(balance.chainId) - const chainDataFromSquid = supportedSquidChainsAndTokens[String(balance.chainId)] - const isTokenSupportedBySquid = - chainDataFromSquid?.tokens.some((squidToken) => - areEvmAddressesEqual(squidToken.address, balance.address) - ) ?? false - return ( - handleTokenSelect(balance)} - isSelected={isSelected} - isEnabled={isTokenSupportedBySquid} - /> - ) - }) - } - return ( - - ) - } + }, [popularTokensList, searchValue]) const popularTokensListTitle = useMemo(() => { - if (searchValue && !isExternalWalletConnected) return 'Search Results' + if (searchValue) return 'Search Results' if (selectedChainID && selectedNetworkName) return `Popular tokens on ${selectedNetworkName}` return 'Popular tokens' - }, [searchValue, selectedChainID, selectedNetworkName, isExternalWalletConnected]) + }, [searchValue, selectedChainID, selectedNetworkName]) const handleClearSelectedToken = useCallback(() => { setSelectedChainID('') @@ -477,13 +288,6 @@ const TokenSelector: React.FC = ({ classNameButton, viewT ) } - // auto disconnect external wallet when claim view is active - useEffect(() => { - if (isExternalWalletConnected && viewType === 'claim') { - disconnectWallet() - } - }, [isExternalWalletConnected, viewType]) - return ( <>
- {/* Popular tokens section - rendered only when there is no wallet connected */} - {showPopularTokensList && ( -
- {selectedNetworkName && clearChainSelection()} - - {filteredPopularTokensToDisplay.length > 0 ? ( - filteredPopularTokensToDisplay.map((token) => { - const isSelected = - !isExternalWalletConnected && - selectedTokenAddress?.toLowerCase() === - token.address.toLowerCase() && - selectedChainID === String(token.chainId) - - return ( - handleTokenSelect(token)} - isSelected={isSelected} - isPopularToken={true} - /> - ) - }) - ) : searchValue ? ( - - ) : ( - - )} - -
- )} - - {/* USER's wallet tokens section - rendered only when there is a wallet connected */} - {showUserTokensList && ( -
- {selectedNetworkName && isExternalWalletConnected && clearChainSelection()} - {renderUserTokenListContent()} -
- )} + {/* Popular tokens section */} +
+ {selectedNetworkName && clearChainSelection()} + + {filteredPopularTokensToDisplay.length > 0 ? ( + filteredPopularTokensToDisplay.map((token) => { + const isSelected = + selectedTokenAddress?.toLowerCase() === + token.address.toLowerCase() && + selectedChainID === String(token.chainId) + + return ( + handleTokenSelect(token)} + isSelected={isSelected} + isPopularToken={true} + /> + ) + }) + ) : searchValue ? ( + + ) : ( + + )} + +
)} diff --git a/src/components/Global/TopNavbar/index.tsx b/src/components/Global/TopNavbar/index.tsx index e82032499..e61c2cf2a 100644 --- a/src/components/Global/TopNavbar/index.tsx +++ b/src/components/Global/TopNavbar/index.tsx @@ -1,5 +1,5 @@ 'use client' -import { getHeaderTitle } from '@/utils' +import { getHeaderTitle } from '@/utils/general.utils' import { usePathname } from 'next/navigation' import LogoutButton from '../LogoutButton' diff --git a/src/components/Global/WalletNavigation/index.tsx b/src/components/Global/WalletNavigation/index.tsx index a509bb0f6..e04c59019 100644 --- a/src/components/Global/WalletNavigation/index.tsx +++ b/src/components/Global/WalletNavigation/index.tsx @@ -3,7 +3,7 @@ import { PEANUT_LOGO } from '@/assets' import DirectSendQr from '@/components/Global/DirectSendQR' import { Icon, type IconName, Icon as NavIcon } from '@/components/Global/Icons/Icon' import underMaintenanceConfig from '@/config/underMaintenance.config' -import { useSupportModalContext } from '@/context/SupportModalContext' +import { useModalsContext } from '@/context/ModalsContext' import { useUserStore } from '@/redux/hooks' import classNames from 'classnames' import Image from 'next/image' @@ -20,13 +20,13 @@ type NavPathProps = { // todo: update icons based on new the design const desktopPaths: NavPathProps[] = [ - { name: 'Send', href: '/send', icon: 'arrow-up-right', size: 10 }, - { name: 'Request', href: '/request', icon: 'arrow-down-left', size: 10 }, - { name: 'Add', href: '/add-money', icon: 'arrow-down', size: 14 }, - { name: 'Withdraw', href: '/withdraw', icon: 'arrow-up', size: 14 }, - { name: 'History', href: '/history', icon: 'history', size: 16 }, - { name: 'Docs', href: 'https://docs.peanut.me/', icon: 'docs', size: 16 }, - { name: 'Support', href: '/support', icon: 'peanut-support', size: 16 }, + { name: 'Send', href: '/send', icon: 'arrow-up-right', size: 14 }, + { name: 'Request', href: '/request', icon: 'arrow-down-left', size: 14 }, + { name: 'Add', href: '/add-money', icon: 'arrow-down', size: 15 }, + { name: 'Withdraw', href: '/withdraw', icon: 'arrow-up', size: 15 }, + { name: 'History', href: '/history', icon: 'history', size: 15 }, + { name: 'Docs', href: 'https://docs.peanut.me/', icon: 'docs', size: 14 }, + { name: 'Support', href: '/support', icon: 'peanut-support', size: 14 }, ] type NavSectionProps = { @@ -69,7 +69,7 @@ type MobileNavProps = { } const MobileNav: React.FC = ({ pathName }) => { - const { setIsSupportModalOpen } = useSupportModalContext() + const { setIsSupportModalOpen } = useModalsContext() const { triggerHaptic } = useHaptic() return ( @@ -84,8 +84,8 @@ const MobileNav: React.FC = ({ pathName }) => { { 'text-primary-1': pathName === '/home' } )} > - - Home + + Home {/* QR Button - Main Action */} diff --git a/src/components/Home/HomeCarouselCTA/CarouselCTA.tsx b/src/components/Home/HomeCarouselCTA/CarouselCTA.tsx index 757fcca60..70794417e 100644 --- a/src/components/Home/HomeCarouselCTA/CarouselCTA.tsx +++ b/src/components/Home/HomeCarouselCTA/CarouselCTA.tsx @@ -1,6 +1,5 @@ 'use client' -import { Card } from '@/components/0_Bruddle' import { Icon, type IconName } from '@/components/Global/Icons/Icon' import type { StaticImageData } from 'next/image' import Image from 'next/image' @@ -9,6 +8,7 @@ import { twMerge } from 'tailwind-merge' import ActionModal from '@/components/Global/ActionModal' import { CAROUSEL_CLOSE_BUTTON_POSITION, CAROUSEL_CLOSE_ICON_SIZE } from '@/constants/carousel.consts' import { useHaptic } from 'use-haptic' +import { Card } from '@/components/0_Bruddle/Card' interface CarouselCTAProps { icon: IconName diff --git a/src/components/Home/HomeHistory.tsx b/src/components/Home/HomeHistory.tsx index b554b7ae9..dc624b698 100644 --- a/src/components/Home/HomeHistory.tsx +++ b/src/components/Home/HomeHistory.tsx @@ -1,6 +1,5 @@ 'use client' -import Icon from '@/components/Global/Icon' import TransactionCard from '@/components/TransactionDetails/TransactionCard' import { mapTransactionDataForDrawer } from '@/components/TransactionDetails/transactionTransformer' import { EHistoryEntryType, type HistoryEntry, useTransactionHistory } from '@/hooks/useTransactionHistory' @@ -20,13 +19,14 @@ import { BadgeStatusItem, isBadgeHistoryItem } from '@/components/Badges/BadgeSt import { useUserInteractions } from '@/hooks/useUserInteractions' import { completeHistoryEntry } from '@/utils/history.utils' import { formatUnits } from 'viem' -import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants' import { useHaptic } from 'use-haptic' +import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' +import { Icon } from '../Global/Icons/Icon' /** * component to display a preview of the most recent transactions on the home page. */ -const HomeHistory = ({ username }: { username?: string }) => { +const HomeHistory = ({ username, hideTxnAmount = false }: { username?: string; hideTxnAmount?: boolean }) => { const { user } = useUserStore() const isLoggedIn = !!user?.user.userId || false // Only filter when user is requesting for some different user's history @@ -333,6 +333,7 @@ const HomeHistory = ({ username }: { username?: string }) => { position={position} isPending={true} haveSentMoneyToUser={transactionDetails.haveSentMoneyToUser} + hideTxnAmount={hideTxnAmount} /> ) })} @@ -344,7 +345,7 @@ const HomeHistory = ({ username }: { username?: string }) => { ) : ( triggerHaptic()}>

Activity

- + )} {/* container for the transaction cards */} @@ -402,6 +403,7 @@ const HomeHistory = ({ username }: { username?: string }) => { transaction={transactionDetails} position={position} haveSentMoneyToUser={haveSentMoneyToUser} + hideTxnAmount={hideTxnAmount} /> ) })} diff --git a/src/components/Home/HomeLink.tsx b/src/components/Home/HomeLink.tsx index bfcd71e7c..f4093c737 100644 --- a/src/components/Home/HomeLink.tsx +++ b/src/components/Home/HomeLink.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames' -import { Card } from '../0_Bruddle' +import { Card } from '@/components/0_Bruddle/Card' import Link from 'next/link' export const HomeLink = ({ diff --git a/src/components/Invites/InvitesPage.tsx b/src/components/Invites/InvitesPage.tsx index 677fbdafb..1f9a8bcc1 100644 --- a/src/components/Invites/InvitesPage.tsx +++ b/src/components/Invites/InvitesPage.tsx @@ -4,7 +4,7 @@ import PeanutLoading from '../Global/PeanutLoading' import ValidationErrorView from '../Payment/Views/Error.validation.view' import InvitesPageLayout from './InvitesPageLayout' import { twMerge } from 'tailwind-merge' -import { Button } from '../0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import peanutAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_01.gif' import { useRouter, useSearchParams } from 'next/navigation' import { invitesApi } from '@/services/invites' @@ -13,7 +13,7 @@ import { useAppDispatch } from '@/redux/hooks' import { setupActions } from '@/redux/slices/setup-slice' import { useAuth } from '@/context/authContext' import { EInviteType } from '@/services/services.types' -import { saveToCookie } from '@/utils' +import { saveToCookie } from '@/utils/general.utils' import { useLogin } from '@/hooks/useLogin' import UnsupportedBrowserModal from '../Global/UnsupportedBrowserModal' diff --git a/src/components/Invites/JoinWaitlistPage.tsx b/src/components/Invites/JoinWaitlistPage.tsx index 92dc56497..789cecc5a 100644 --- a/src/components/Invites/JoinWaitlistPage.tsx +++ b/src/components/Invites/JoinWaitlistPage.tsx @@ -2,17 +2,19 @@ import { useAuth } from '@/context/authContext' import { invitesApi } from '@/services/invites' -import React, { useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import InvitesPageLayout from './InvitesPageLayout' import { twMerge } from 'tailwind-merge' import ValidatedInput from '../Global/ValidatedInput' -import { Button } from '../0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import ErrorAlert from '../Global/ErrorAlert' import peanutAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_02.gif' +import chillPeanutAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_01.gif' import { useRouter } from 'next/navigation' import { useQuery } from '@tanstack/react-query' import PeanutLoading from '../Global/PeanutLoading' import { useSetupStore } from '@/redux/hooks' +import { useNotifications } from '@/hooks/useNotifications' const JoinWaitlistPage = () => { const [isValid, setIsValid] = useState(false) @@ -25,6 +27,8 @@ const JoinWaitlistPage = () => { const { inviteType, inviteCode: setupInviteCode } = useSetupStore() const [inviteCode, setInviteCode] = useState(setupInviteCode) + const { requestPermission, afterPermissionAttempt, isPermissionGranted } = useNotifications() + const { data, isLoading: isLoadingWaitlistPosition } = useQuery({ queryKey: ['waitlist-position'], queryFn: () => invitesApi.getWaitlistQueuePosition(), @@ -73,7 +77,7 @@ const JoinWaitlistPage = () => { } return ( - +
{ )} >
-
-

You're still in Peanut jail

- -

Prisoner #{data?.position}

-

- No bail without an invite. Got a code? Prove it below. No code? Back to the waitlist. Go beg - your friend! -

- -
- { - setIsValid(isValid) - setIsChanging(isChanging) - setInviteCode(value) - }} - isSetupFlow - isInputChanging={isChanging} - className={twMerge( - !isValid && !isChanging && !!inviteCode && 'border-error dark:border-error', - isValid && - !isChanging && - !!inviteCode && - 'border-secondary-8 dark:border-secondary-8', - 'rounded-sm' - )} - /> + {!isPermissionGranted && ( +
+

Enable notifications

+

We'll send you an update as soon as you get access.

- - {!isValid && !isChanging && !!inviteCode && ( - - )} - - {/* Show error from the API call */} - {error && } - - -
+ )} + + {isPermissionGranted && ( +
+

You're still in Peanut jail

+ +

Prisoner #{data?.position}

+

+ No bail without an invite. Got a code? Prove it below. No code? Back to the waitlist. Go + beg your friend! +

+ +
+ { + setIsValid(isValid) + setIsChanging(isChanging) + setInviteCode(value) + }} + isSetupFlow + isInputChanging={isChanging} + className={twMerge( + !isValid && !isChanging && !!inviteCode && 'border-error dark:border-error', + isValid && + !isChanging && + !!inviteCode && + 'border-secondary-8 dark:border-secondary-8', + 'rounded-sm' + )} + /> + + +
+ + {!isValid && !isChanging && !!inviteCode && ( + + )} + + {/* Show error from the API call */} + {error && } + + +
+ )}
diff --git a/src/components/Kyc/InitiateBridgeKYCModal.tsx b/src/components/Kyc/InitiateBridgeKYCModal.tsx index 8fbca218c..0fdb440bf 100644 --- a/src/components/Kyc/InitiateBridgeKYCModal.tsx +++ b/src/components/Kyc/InitiateBridgeKYCModal.tsx @@ -3,7 +3,7 @@ import { useBridgeKycFlow } from '@/hooks/useBridgeKycFlow' import IframeWrapper from '@/components/Global/IframeWrapper' import { KycVerificationInProgressModal } from './KycVerificationInProgressModal' import { type IconName } from '@/components/Global/Icons/Icon' -import { saveRedirectUrl } from '@/utils' +import { saveRedirectUrl } from '@/utils/general.utils' import useClaimLink from '../Claim/useClaimLink' interface BridgeKycModalFlowProps { diff --git a/src/components/Kyc/InitiateMantecaKYCModal.tsx b/src/components/Kyc/InitiateMantecaKYCModal.tsx index ca854e913..d72f425cc 100644 --- a/src/components/Kyc/InitiateMantecaKYCModal.tsx +++ b/src/components/Kyc/InitiateMantecaKYCModal.tsx @@ -5,7 +5,7 @@ import IframeWrapper from '@/components/Global/IframeWrapper' import { type IconName } from '@/components/Global/Icons/Icon' import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow' import { type CountryData } from '@/components/AddMoney/consts' -import { Button } from '../0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { PeanutDoesntStoreAnyPersonalInformation } from './KycVerificationInProgressModal' import { useEffect } from 'react' @@ -19,6 +19,7 @@ interface Props { description?: string | React.ReactNode ctaText?: string footer?: React.ReactNode + autoStartKyc?: boolean } const InitiateMantecaKYCModal = ({ @@ -31,6 +32,7 @@ const InitiateMantecaKYCModal = ({ description, ctaText, footer, + autoStartKyc, }: Props) => { const { isLoading, iframeOptions, openMantecaKyc, handleIframeClose } = useMantecaKycFlow({ onClose: onManualClose, // any non-success close from iframe is a manual close in case of Manteca KYC @@ -53,16 +55,26 @@ const InitiateMantecaKYCModal = ({ } }, []) + useEffect(() => { + if (autoStartKyc) { + openMantecaKyc(country) + } + }, [autoStartKyc]) + + const isAutoStarting = autoStartKyc && isLoading + const displayTitle = isAutoStarting ? 'Starting verification...' : (title ?? 'Verify your identity first') + const displayDescription = isAutoStarting + ? 'Please wait while we start your verification...' + : (description ?? + 'To continue, you need to complete identity verification. This usually takes just a few minutes.') + return ( <> { return ( - {({ onClose }) => ( + + {({ close }) => ( <> - {trigger} - - -
-
-
- -
- setSearchTerm(e.target.value)} - className="h-10 w-full rounded-sm border-[1.15px] border-black pl-10 pr-10 font-normal caret-[#FF90E8] focus:border-black focus:outline-none focus:ring-0" - /> + {trigger} + +
+
+
+
+ setSearchTerm(e.target.value)} + className="h-10 w-full rounded-sm border-[1.15px] border-black pl-10 pr-10 font-normal caret-[#FF90E8] focus:border-black focus:outline-none focus:ring-0" + /> +
-
- {!searchTerm && ( -

- Popular currencies -

- )} - {filteredCurrencies - .filter((currency) => popularCurrencies.includes(currency.currency)) - .map((currency, index) => ( - { - if (!currency.comingSoon) { - onClose() - setSelectedCurrency(currency.currency) - } - }} - /> - ))} +
+ {!searchTerm && ( +

Popular currencies

+ )} + {filteredCurrencies + .filter((currency) => popularCurrencies.includes(currency.currency)) + .map((currency, index) => ( + { + if (!currency.comingSoon) { + close() + setSelectedCurrency(currency.currency) + } + }} + /> + ))} - {!searchTerm && ( -

All currencies

- )} - {filteredCurrencies - .filter((currency) => !popularCurrencies.includes(currency.currency)) - .map((currency, index) => ( - { - if (!currency.comingSoon) { - onClose() - setSelectedCurrency(currency.currency) - } - }} - /> - ))} -
+ {!searchTerm && ( +

All currencies

+ )} + {filteredCurrencies + .filter((currency) => !popularCurrencies.includes(currency.currency)) + .map((currency, index) => ( + { + if (!currency.comingSoon) { + close() + setSelectedCurrency(currency.currency) + } + }} + /> + ))}
- - +
+
)} diff --git a/src/components/LandingPage/TweetCarousel.tsx b/src/components/LandingPage/TweetCarousel.tsx index 29b2e8ba3..5db714d10 100644 --- a/src/components/LandingPage/TweetCarousel.tsx +++ b/src/components/LandingPage/TweetCarousel.tsx @@ -1,8 +1,8 @@ 'use client' +import { TWEETS, type Tweet } from '@/constants/tweets.consts' import { useMemo, useState } from 'react' import Marquee from 'react-fast-marquee' -import { TWEETS, type Tweet } from '@/constants' // ============================================================================= // Constants @@ -328,7 +328,7 @@ const buildColumns = (tweets: Tweet[]): ColumnType[] => { * - Lazy loaded images with fallbacks * - Links open in new tab */ -export const TweetCarousel = () => { +const TweetCarousel = () => { const columns = useMemo(() => buildColumns(TWEETS), []) if (columns.length === 0) return null diff --git a/src/components/LandingPage/dropLink.tsx b/src/components/LandingPage/dropLink.tsx index d3d97a7fd..0f10006ce 100644 --- a/src/components/LandingPage/dropLink.tsx +++ b/src/components/LandingPage/dropLink.tsx @@ -1,7 +1,7 @@ 'use client' import Image from 'next/image' import { motion } from 'framer-motion' -import { Button } from '../0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import iphoneDropALink from '@/assets/iphone-ss/iphone-drop-a-link.png' import iphoneDropALinkMobile from '@/assets/iphone-ss/iphone-drop-a-link-mobile.png' import { WHATSAPP_ICON, IMESSAGE_ICON, FBMessenger_ICON, TELEGRAM_ICON } from '@/assets/icons' diff --git a/src/components/LandingPage/faq.tsx b/src/components/LandingPage/faq.tsx index f47d3d668..27ec5e5de 100644 --- a/src/components/LandingPage/faq.tsx +++ b/src/components/LandingPage/faq.tsx @@ -2,8 +2,6 @@ import { Eyes, PeanutsBG } from '@/assets' import { MarqueeComp } from '@/components/Global/MarqueeWrapper' -import { Box } from '@chakra-ui/react' -import { useState } from 'react' import { FAQsPanel, type FAQsProps } from '../Global/FAQs' type LocalFAQsProps = FAQsProps & { @@ -14,14 +12,8 @@ type LocalFAQsProps = FAQsProps & { } export function FAQs({ heading, questions, marquee = { visible: false } }: LocalFAQsProps) { - const [openFaq, setOpenFaq] = useState(-1) - - const setFaq = function (index: number) { - setOpenFaq(openFaq === index ? -1 : index) - } - return ( - )} - +
) } diff --git a/src/components/LandingPage/hero.tsx b/src/components/LandingPage/hero.tsx index 89aa52feb..eb851376d 100644 --- a/src/components/LandingPage/hero.tsx +++ b/src/components/LandingPage/hero.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react' import { twMerge } from 'tailwind-merge' import { CloudImages, HeroImages } from './imageAssets' import Image from 'next/image' -import { Button } from '../0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' type CTAButton = { label: string diff --git a/src/components/LandingPage/index.ts b/src/components/LandingPage/index.ts index 17bdddcac..3288a182e 100644 --- a/src/components/LandingPage/index.ts +++ b/src/components/LandingPage/index.ts @@ -3,9 +3,7 @@ export * from './faq' export * from './hero' export * from './marquee' export * from './noFees' -export * from './nutsDivider' export * from './securityBuiltIn' export * from './sendInSeconds' export * from './yourMoney' export * from './RegulatedRails' -export * from './TweetCarousel' diff --git a/src/components/LandingPage/noFees.tsx b/src/components/LandingPage/noFees.tsx index 388eb3419..4b830ac6c 100644 --- a/src/components/LandingPage/noFees.tsx +++ b/src/components/LandingPage/noFees.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { motion } from 'framer-motion' import borderCloud from '@/assets/illustrations/border-cloud.svg' import noHiddenFees from '@/assets/illustrations/no-hidden-fees.svg' @@ -8,7 +8,7 @@ import { Star } from '@/assets' import Image from 'next/image' import ExchangeRateWidget from '../Global/ExchangeRateWidget' import { useRouter } from 'next/navigation' -import { printableUsdc } from '@/utils' +import { printableUsdc } from '@/utils/balance.utils' import { getExchangeRateWidgetRedirectRoute } from '@/utils/exchangeRateWidget.utils' import { useWallet } from '@/hooks/wallet/useWallet' import { useAuth } from '@/context/authContext' diff --git a/src/components/LandingPage/nutsDivider.tsx b/src/components/LandingPage/nutsDivider.tsx deleted file mode 100644 index 1241425e9..000000000 --- a/src/components/LandingPage/nutsDivider.tsx +++ /dev/null @@ -1,21 +0,0 @@ -'use client' - -import { Box } from '@chakra-ui/react' -import { PeanutsBG } from '@/assets' - -type DividerProps = { - height?: string - className?: string -} - -export function NutsDivider({ height = 'h-[10vw] md:h-[7vw]', className }: DividerProps) { - const inlineStyle = { - backgroundImage: `url(${PeanutsBG.src})`, - backgroundSize: '8rem auto', - backgroundRepeat: 'repeat', - } - - const boxClass = `${height} grow border-4 border-n-1 bg-primary ring-2 ring-white shadow-md` - - return -} diff --git a/src/components/LandingPage/securityBuiltIn.tsx b/src/components/LandingPage/securityBuiltIn.tsx index 3cd1fb552..5f48b8d82 100644 --- a/src/components/LandingPage/securityBuiltIn.tsx +++ b/src/components/LandingPage/securityBuiltIn.tsx @@ -1,5 +1,5 @@ import Image from 'next/image' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import handThumbsUp from '@/assets/illustrations/hand-thumbs-up.svg' import handWaving from '@/assets/illustrations/hand-waving.svg' import handPeace from '@/assets/illustrations/hand-peace.svg' diff --git a/src/components/LandingPage/sendInSeconds.tsx b/src/components/LandingPage/sendInSeconds.tsx index 62df81d22..a83c36602 100644 --- a/src/components/LandingPage/sendInSeconds.tsx +++ b/src/components/LandingPage/sendInSeconds.tsx @@ -6,7 +6,7 @@ import exclamations from '@/assets/illustrations/exclamations.svg' import payZeroFees from '@/assets/illustrations/pay-zero-fees.svg' import mobileSendInSeconds from '@/assets/illustrations/mobile-send-in-seconds.svg' import { Star, Sparkle } from '@/assets' -import { Button } from '../0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' export function SendInSeconds() { const [screenWidth, setScreenWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1200) diff --git a/src/components/LandingPage/yourMoney.tsx b/src/components/LandingPage/yourMoney.tsx index c2121a6f8..a5875c6c5 100644 --- a/src/components/LandingPage/yourMoney.tsx +++ b/src/components/LandingPage/yourMoney.tsx @@ -1,6 +1,6 @@ import Image from 'next/image' import { LandingCountries } from '@/assets' -import { Button } from '../0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' export function YourMoney() { return ( diff --git a/src/components/Offramp/Offramp.consts.ts b/src/components/Offramp/Offramp.consts.ts index 6b5f74226..4248c16cc 100644 --- a/src/components/Offramp/Offramp.consts.ts +++ b/src/components/Offramp/Offramp.consts.ts @@ -1,6 +1,6 @@ -import * as consts from '@/constants' import * as interfaces from '@/interfaces' import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' +import type { IOfframpForm } from '@/constants/cashout.consts' export interface CrossChainDetails { chainId: string @@ -34,8 +34,8 @@ export interface IOfframpConfirmScreenProps { // available in all offramp types onPrev: () => void onNext: () => void - offrampForm: consts.IOfframpForm - setOfframpForm: (form: consts.IOfframpForm) => void + offrampForm: IOfframpForm + setOfframpForm: (form: IOfframpForm) => void initialKYCStep: number setTransactionHash: (hash: string) => void offrampType: OfframpType @@ -68,7 +68,7 @@ export interface IOfframpConfirmScreenProps { export interface IOfframpSuccessScreenProps { // available in all offramp types - offrampForm: consts.IOfframpForm + offrampForm: IOfframpForm offrampType: OfframpType // available in cashout offramps diff --git a/src/components/Payment/PaymentAmountInput/index.tsx b/src/components/Payment/PaymentAmountInput/index.tsx deleted file mode 100644 index 7e9623aba..000000000 --- a/src/components/Payment/PaymentAmountInput/index.tsx +++ /dev/null @@ -1,420 +0,0 @@ -'use client' - -import { PEANUT_WALLET_TOKEN_DECIMALS, STABLE_COINS } from '@/constants' -import { tokenSelectorContext } from '@/context' -import { formatTokenAmount, formatCurrency } from '@/utils' -import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' -import { useRouter, useSearchParams } from 'next/navigation' -import { Icon } from '@/components/Global/Icons/Icon' -import { twMerge } from 'tailwind-merge' -import { Icon as IconComponent } from '@/components/Global/Icons/Icon' -import { Slider } from '@/components/Global/Slider' -import { DeviceType, useDeviceType } from '@/hooks/useGetDeviceType' - -interface PaymentAmountInputProps { - className?: string - tokenValue: string | undefined - setTokenValue: (tokenvalue: string | undefined) => void - setUsdValue?: (usdvalue: string) => void - setCurrencyAmount?: (currencyvalue: string | undefined) => void - onSubmit?: () => void - onBlur?: () => void - disabled?: boolean - walletBalance?: string - currency?: { - code: string - symbol: string - price: number - } - hideCurrencyToggle?: boolean - hideBalance?: boolean - showInfoText?: boolean - infoText?: string - showSlider?: boolean - maxAmount?: number - amountCollected?: number - isInitialInputUsd?: boolean - defaultSliderValue?: number - defaultSliderSuggestedAmount?: number -} - -/** - * this is a shadow clone of TokenAmountInput, it is used to display the amount in the payment form, to deal with the useeffect fuckup in that component using this component temperiorly so that we can fix the useeffect fuckup in that component next week - */ - -const PaymentAmountInput = ({ - className, - tokenValue, - setTokenValue, - setCurrencyAmount, - onSubmit, - onBlur, - disabled, - walletBalance, - currency, - setUsdValue, - hideCurrencyToggle = false, - hideBalance = false, - infoText, - showInfoText, - showSlider = false, - maxAmount, - amountCollected = 0, - isInitialInputUsd = false, - defaultSliderValue, - defaultSliderSuggestedAmount, -}: PaymentAmountInputProps) => { - const { selectedTokenData } = useContext(tokenSelectorContext) - const router = useRouter() - const searchParams = useSearchParams() - const inputRef = useRef(null) - const inputType = useMemo(() => (window.innerWidth < 640 ? 'text' : 'number'), []) - const [isFocused, setIsFocused] = useState(false) - const { deviceType } = useDeviceType() - // Only autofocus on desktop (WEB), not on mobile devices (IOS/ANDROID) - const shouldAutoFocus = deviceType === DeviceType.WEB - - // Store display value for input field (what user sees when typing) - const [displayValue, setDisplayValue] = useState(tokenValue || '') - const [isInputUsd, setIsInputUsd] = useState(!currency || isInitialInputUsd) - const [displaySymbol, setDisplaySymbol] = useState('') - const [alternativeDisplayValue, setAlternativeDisplayValue] = useState('0.00') - const [alternativeDisplaySymbol, setAlternativeDisplaySymbol] = useState('') - - const displayMode = useMemo<'TOKEN' | 'STABLE' | 'FIAT'>(() => { - if (currency) return 'FIAT' - if (selectedTokenData?.symbol && STABLE_COINS.includes(selectedTokenData?.symbol)) { - return 'STABLE' - } else { - return 'TOKEN' - } - }, [currency, selectedTokenData?.symbol]) - - const decimals = useMemo(() => { - let _decimals: number - if (displayMode === 'TOKEN' && isInputUsd && selectedTokenData?.decimals) { - _decimals = selectedTokenData.decimals - } else { - _decimals = PEANUT_WALLET_TOKEN_DECIMALS - } - // For displaying the token amount, anything more breaks the UI - return 6 < _decimals ? 6 : _decimals - }, [displayMode, selectedTokenData?.decimals, isInputUsd]) - - const calculateAlternativeValue = useCallback( - (value: string) => { - // There is no alternative display when dealing with stables - if (displayMode === 'STABLE') return '' - - let price: number - if (displayMode === 'TOKEN') { - if (!selectedTokenData?.price) return '' - price = selectedTokenData.price - } else if (displayMode === 'FIAT') { - if (!currency?.price) return '' - price = 1 / currency.price - } else { - throw new Error('Invalid display mode') - } - - if (isInputUsd) { - return formatTokenAmount(Number(value) / price, decimals)! - } else { - return formatTokenAmount(Number(value) * price, decimals)! - } - }, - [displayMode, currency?.price, selectedTokenData?.price, isInputUsd, decimals] - ) - - const onChange = useCallback( - (value: string, _isInputUsd: boolean) => { - setDisplayValue(value) - setAlternativeDisplayValue(calculateAlternativeValue(value)) - let tokenValue: string - switch (displayMode) { - case 'STABLE': { - tokenValue = value - break - } - case 'TOKEN': { - tokenValue = _isInputUsd ? (Number(value) / (selectedTokenData?.price ?? 1)).toString() : value - break - } - case 'FIAT': { - if (!currency?.price) throw new Error('Invalid currency') - const usdValue = _isInputUsd ? Number(value) : Number(value) / currency.price - // For when we have a non stablecoin request in currency mode - tokenValue = formatTokenAmount(usdValue / (selectedTokenData?.price ?? 1), decimals)! - const currencyValue = _isInputUsd ? (Number(value) * currency.price).toString() : value - setCurrencyAmount?.(currencyValue) - break - } - default: { - throw new Error('Invalid display mode') - } - } - setTokenValue(tokenValue) - }, - [displayMode, currency?.price, selectedTokenData?.price, calculateAlternativeValue] - ) - - const onSliderValueChange = useCallback( - (value: number[]) => { - if (maxAmount) { - const selectedPercentage = value[0] - let selectedAmount = (selectedPercentage / 100) * maxAmount - - // Only snap to exact remaining amount when user selects the 33.33% magnetic snap point - // This ensures equal splits fill the pot exactly to 100% - const SNAP_POINT_TOLERANCE = 0.5 // percentage points - allows magnetic snapping - const COMPLETION_THRESHOLD = 0.98 // 98% - if 33.33% would nearly complete pot - const EQUAL_SPLIT_PERCENTAGE = 100 / 3 // 33.333...% - - const isAt33SnapPoint = Math.abs(selectedPercentage - EQUAL_SPLIT_PERCENTAGE) < SNAP_POINT_TOLERANCE - if (isAt33SnapPoint && amountCollected > 0) { - const remainingAmount = maxAmount - amountCollected - // Only snap if there's remaining amount and 33.33% would nearly complete the pot - if (remainingAmount > 0 && selectedAmount >= remainingAmount * COMPLETION_THRESHOLD) { - selectedAmount = remainingAmount - } - } - - const selectedAmountStr = parseFloat(selectedAmount.toFixed(4)).toString() - const maxDecimals = displayMode === 'FIAT' || displayMode === 'STABLE' || isInputUsd ? 2 : decimals - const formattedAmount = formatTokenAmount(selectedAmountStr, maxDecimals, true) - if (formattedAmount) { - onChange(formattedAmount, isInputUsd) - } - } - }, - [maxAmount, amountCollected, onChange, displayMode, isInputUsd, decimals] - ) - - const showConversion = useMemo(() => { - return !hideCurrencyToggle && (displayMode === 'TOKEN' || displayMode === 'FIAT') - }, [hideCurrencyToggle, displayMode]) - - // This is needed because if we change the token we selected the value - // should change. This only depends on the price on purpose!! we don't want - // to change when we change the display mode or the value (we already call - // onchange on the input change so dont add those dependencies here!) - useEffect(() => { - // early return if tokenValue is empty. - if (!tokenValue) return - - if (!isInitialInputUsd) { - const value = tokenValue ? Number(tokenValue) : 0 - const formattedValue = (value * (currency?.price ?? 1)).toFixed(2) - onChange(formattedValue, isInputUsd) - } else { - onChange(displayValue, isInputUsd) - } - }, [selectedTokenData?.price, displayValue, isInputUsd]) - - useEffect(() => { - switch (displayMode) { - case 'STABLE': { - setDisplaySymbol('$') - setAlternativeDisplaySymbol('') - break - } - case 'TOKEN': { - if (isInputUsd) { - setDisplaySymbol('$') - setAlternativeDisplaySymbol(selectedTokenData?.symbol || '') - } else { - setDisplaySymbol(selectedTokenData?.symbol || '') - setAlternativeDisplaySymbol('$') - } - break - } - case 'FIAT': { - if (isInputUsd) { - setDisplaySymbol('USD') - setAlternativeDisplaySymbol(currency?.symbol || '') - } else { - setDisplaySymbol(currency?.symbol || '') - setAlternativeDisplaySymbol('USD') - } - break - } - default: { - throw new Error('Invalid display mode') - } - } - }, [displayMode, selectedTokenData?.symbol, currency?.symbol, isInputUsd]) - - useEffect(() => { - if (inputRef.current) { - if (displayValue?.length !== 0) { - inputRef.current.style.width = `${displayValue?.length ?? 0}ch` - } else { - inputRef.current.style.width = `4ch` - } - } - }, [displayValue, currency]) - - useEffect(() => { - if (!setUsdValue) return - if (displayMode === 'STABLE') { - setUsdValue(displayValue) - } else { - setUsdValue(isInputUsd ? displayValue : alternativeDisplayValue) - } - }, [setUsdValue, displayValue, alternativeDisplayValue, isInputUsd, displayMode]) - - const formRef = useRef(null) - - const sliderValue = useMemo(() => { - if (!maxAmount || !tokenValue) return [0] - const tokenNum = parseFloat(tokenValue.replace(/,/g, '')) - const usdValue = tokenNum * (selectedTokenData?.price ?? 1) - return [(usdValue / maxAmount) * 100] - }, [maxAmount, tokenValue, selectedTokenData?.price]) - - const handleContainerClick = () => { - if (inputRef.current) { - inputRef.current.focus() - } - } - - // Sync default slider suggested amount to the input - useEffect(() => { - if (defaultSliderSuggestedAmount) { - const formattedAmount = formatTokenAmount(defaultSliderSuggestedAmount.toString(), 2) - if (formattedAmount) { - setTokenValue(formattedAmount) - setDisplayValue(formattedAmount) - } - } - }, [defaultSliderSuggestedAmount]) - - return ( -
-
-
- - - {/* Input with fake caret */} -
- { - let value = e.target.value - // USD/currency โ†’ 2 decimals; token input โ†’ allow `decimals` (<= 6) - const maxDecimals = - displayMode === 'FIAT' || displayMode === 'STABLE' || isInputUsd ? 2 : decimals - const formattedAmount = formatTokenAmount(value, maxDecimals, true) - if (formattedAmount !== undefined) { - value = formattedAmount - } - onChange(value, isInputUsd) - }} - ref={inputRef} - inputMode="decimal" - type={inputType} - value={displayValue.replace(/,/g, '')} - step="any" - min="0" - autoComplete="off" - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault() - if (onSubmit) onSubmit() - } - }} - onFocus={() => setIsFocused(true)} - onBlur={() => { - setIsFocused(false) - if (onBlur) onBlur() - }} - disabled={disabled} - /> - {/* Fake blinking caret shown when not focused and input is empty */} - {!isFocused && !displayValue && ( -
- )} -
-
- - {/* Conversion */} - {showConversion && ( - - )} - - {/* Balance */} - {walletBalance && !hideBalance && ( -
- Balance: {displayMode === 'FIAT' && currency ? 'USD ' : '$ '} - {walletBalance} -
- )} -
- {/* Conversion toggle */} - {showConversion && ( -
{ - e.preventDefault() - const currentValue = displayValue - if (!alternativeDisplayValue || alternativeDisplayValue === '0.00') { - setDisplayValue('') - } else { - setDisplayValue(alternativeDisplayValue) - } - if (!currentValue) { - setAlternativeDisplayValue('0.00') - } else { - setAlternativeDisplayValue(currentValue) - } - setIsInputUsd(!isInputUsd) - - // Toggle swap-currency parameter in URL - const params = new URLSearchParams(searchParams.toString()) - const currentSwapValue = params.get('swap-currency') - if (currentSwapValue === 'true') { - params.set('swap-currency', 'false') - } else { - params.set('swap-currency', 'true') - } - router.replace(`?${params.toString()}`, { scroll: false }) - }} - > - -
- )} - {showInfoText && infoText && ( -
- -

{infoText}

-
- )} - {showSlider && maxAmount && ( -
- -
- )} - - ) -} - -export default PaymentAmountInput diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx deleted file mode 100644 index 8c3bf8e09..000000000 --- a/src/components/Payment/PaymentForm/index.tsx +++ /dev/null @@ -1,1000 +0,0 @@ -'use client' - -import { PEANUT_LOGO_BLACK } from '@/assets' -import { PEANUTMAN_LOGO } from '@/assets/peanut' -import { Button } from '@/components/0_Bruddle' -import ActionModal from '@/components/Global/ActionModal' -import AddressLink from '@/components/Global/AddressLink' -import ErrorAlert from '@/components/Global/ErrorAlert' -import FileUploadInput from '@/components/Global/FileUploadInput' -import { type IconName } from '@/components/Global/Icons/Icon' -import NavHeader from '@/components/Global/NavHeader' -import PaymentAmountInput from '@/components/Payment/PaymentAmountInput' -import TokenSelector from '@/components/Global/TokenSelector/TokenSelector' -import UserCard from '@/components/User/UserCard' -import { PEANUT_WALLET_TOKEN, PEANUT_WALLET_TOKEN_DECIMALS, PEANUT_WALLET_CHAIN } from '@/constants' -import { tokenSelectorContext } from '@/context' -import { useAuth } from '@/context/authContext' -import { useRequestFulfillmentFlow } from '@/context/RequestFulfillmentFlowContext' -import { type InitiatePaymentPayload, usePaymentInitiator } from '@/hooks/usePaymentInitiator' -import { useWallet } from '@/hooks/wallet/useWallet' -import { usePendingTransactions } from '@/hooks/wallet/usePendingTransactions' -import { useTokenPrice } from '@/hooks/useTokenPrice' -import { useSquidChainsAndTokens } from '@/hooks/useSquidChainsAndTokens' -import { type ParsedURL } from '@/lib/url-parser/types/payment' -import { useAppDispatch, usePaymentStore } from '@/redux/hooks' -import { paymentActions } from '@/redux/slices/payment-slice' -import { walletActions } from '@/redux/slices/wallet-slice' -import { areEvmAddressesEqual, ErrorHandler, formatAmount, formatCurrency, getContributorsFromCharge } from '@/utils' -import { useAppKit, useDisconnect } from '@reown/appkit/react' -import { initializeAppKit } from '@/config/wagmi.config' -import Image from 'next/image' -import { useRouter, useSearchParams } from 'next/navigation' -import { useCallback, useContext, useEffect, useMemo, useState } from 'react' -import { formatUnits } from 'viem' -import { useAccount } from 'wagmi' -import { useUserInteractions } from '@/hooks/useUserInteractions' -import { useUserByUsername } from '@/hooks/useUserByUsername' -import { type PaymentFlow } from '@/app/[...recipient]/client' -import { invitesApi } from '@/services/invites' -import { EInviteType } from '@/services/services.types' -import ContributorCard from '@/components/Global/Contributors/ContributorCard' -import { getCardPosition } from '@/components/Global/Card' -import * as Sentry from '@sentry/nextjs' -import { useHaptic } from 'use-haptic' -import TokenAmountInput from '@/components/Global/TokenAmountInput' - -export type PaymentFlowProps = { - isExternalWalletFlow?: boolean - /** Whether this is a direct USD payment flow (bypasses token conversion) */ - isDirectUsdPayment?: boolean - currency?: { - code: string - symbol: string - price: number - } - currencyAmount?: string - setCurrencyAmount?: (currencyvalue: string | undefined) => void - headerTitle?: string - flow?: PaymentFlow - showRequestPotInitialView?: boolean -} - -export type PaymentFormProps = ParsedURL & PaymentFlowProps - -export const PaymentForm = ({ - recipient, - amount, - token, - chain, - currency, - currencyAmount, - setCurrencyAmount, - isExternalWalletFlow, - isDirectUsdPayment, - headerTitle, - flow, - showRequestPotInitialView, -}: PaymentFormProps) => { - const dispatch = useAppDispatch() - const router = useRouter() - const { user, fetchUser } = useAuth() - const { - requestDetails, - chargeDetails, - daimoError, - error: paymentStoreError, - attachmentOptions, - currentView, - parsedPaymentData, - } = usePaymentStore() - const { triggerPayWithPeanut, setTriggerPayWithPeanut } = useRequestFulfillmentFlow() - const recipientUsername = !chargeDetails && recipient?.recipientType === 'USERNAME' ? recipient.identifier : null - const { user: recipientUser } = useUserByUsername(recipientUsername) - - const recipientUserId = - requestDetails?.recipientAccount?.userId || - chargeDetails?.requestLink.recipientAccount.userId || - recipientUser?.userId - const recipientKycStatus = - chargeDetails?.requestLink.recipientAccount.user?.bridgeKycStatus || recipientUser?.bridgeKycStatus - - const { interactions } = useUserInteractions(recipientUserId ? [recipientUserId] : []) - const { isConnected: isPeanutWalletConnected, balance } = useWallet() - const { isConnected: isExternalWalletConnected, status } = useAccount() - - // Fetch Squid chains and tokens for token price lookup - const { data: supportedSquidChainsAndTokens = {} } = useSquidChainsAndTokens() - - // Fetch token price for request details (xchain requests) - const { data: requestedTokenPriceData } = useTokenPrice({ - tokenAddress: requestDetails?.tokenAddress, - chainId: requestDetails?.chainId, - supportedSquidChainsAndTokens, - isPeanutWallet: false, // Request details are always external tokens - }) - const [initialSetupDone, setInitialSetupDone] = useState(false) - const [inputTokenAmount, setInputTokenAmount] = useState( - chargeDetails?.tokenAmount || requestDetails?.tokenAmount || amount || '' - ) - const [isAcceptingInvite, setIsAcceptingInvite] = useState(false) - const [inviteError, setInviteError] = useState(false) - - // states - const [disconnectWagmiModal, setDisconnectWagmiModal] = useState(false) - const [inputUsdValue, setInputUsdValue] = useState('') - const [usdValue, setUsdValue] = useState('') - - const { initiatePayment, isProcessing, error: initiatorError } = usePaymentInitiator() - const { hasPendingTransactions } = usePendingTransactions() - - const peanutWalletBalance = useMemo(() => { - return balance !== undefined ? formatCurrency(formatUnits(balance, PEANUT_WALLET_TOKEN_DECIMALS)) : '' - }, [balance]) - - const error = useMemo(() => { - if (paymentStoreError) return ErrorHandler(paymentStoreError) - if (initiatorError) return ErrorHandler(initiatorError) - if (inviteError) return 'Something went wrong. Please try again or contact support.' - return null - }, [paymentStoreError, initiatorError, inviteError]) - - const { - selectedChainID, - selectedTokenAddress, - selectedTokenData, - setSelectedChainID, - setSelectedTokenAddress, - selectedTokenBalance, - } = useContext(tokenSelectorContext) - const { open: openReownModal } = useAppKit() - const { disconnect: disconnectWagmi } = useDisconnect() - const { address: wagmiAddress } = useAccount() - const searchParams = useSearchParams() - const requestId = searchParams.get('id') - const isDepositRequest = searchParams.get('action') === 'deposit' - const { triggerHaptic } = useHaptic() - - const isUsingExternalWallet = useMemo(() => { - return isExternalWalletFlow || !isPeanutWalletConnected - }, [isPeanutWalletConnected, isExternalWalletFlow]) - - const isConnected = useMemo(() => { - return isPeanutWalletConnected || isExternalWalletConnected - }, [isPeanutWalletConnected, isExternalWalletConnected, status]) - - const isActivePeanutWallet = useMemo(() => !!user && isPeanutWalletConnected, [user, isPeanutWalletConnected]) - - const isRequestPotLink = !!chargeDetails?.requestLink - - useEffect(() => { - // skip this step for request pot payments - // Amount is set by the user so we dont need to manually update it - // chain and token are also USDC arb always, for cross-chain we use Daimo - if (initialSetupDone || showRequestPotInitialView || isRequestPotLink) return - - // prioritize charge amount over URL amount - if (chargeDetails?.tokenAmount) { - setInputTokenAmount(chargeDetails.tokenAmount) - } else if (amount) { - setInputTokenAmount(amount) - } - - // for ADDRESS/ENS recipients, initialize token/chain from URL or defaults - const isExternalRecipient = recipient?.recipientType === 'ADDRESS' || recipient?.recipientType === 'ENS' - - if (chain) { - setSelectedChainID((chain.chainId || requestDetails?.chainId) ?? '') - if (!token && !requestDetails?.tokenAddress) { - const defaultToken = chain.tokens.find((t) => t.symbol.toLowerCase() === 'usdc') - if (defaultToken) { - setSelectedTokenAddress(defaultToken.address) - // Note: decimals automatically derived by useTokenPrice hook - } - } - } else if (isExternalRecipient && !selectedChainID) { - // default to arbitrum for external recipients if no chain specified - setSelectedChainID(PEANUT_WALLET_CHAIN.id.toString()) - } - - if (token) { - setSelectedTokenAddress((token.address || requestDetails?.tokenAddress) ?? '') - // Note: decimals automatically derived by useTokenPrice hook - } else if (isExternalRecipient && !selectedTokenAddress && selectedChainID) { - // default to USDC for external recipients if no token specified - const chainData = supportedSquidChainsAndTokens[selectedChainID] - const defaultToken = chainData?.tokens.find((t) => t.symbol.toLowerCase() === 'usdc') - if (defaultToken) { - setSelectedTokenAddress(defaultToken.address) - } - } - - setInitialSetupDone(true) - }, [ - chain, - token, - amount, - initialSetupDone, - requestDetails, - showRequestPotInitialView, - isRequestPotLink, - recipient?.recipientType, - selectedChainID, - selectedTokenAddress, - supportedSquidChainsAndTokens, - ]) - - // reset error when component mounts or recipient changes - useEffect(() => { - dispatch(paymentActions.setError(null)) - dispatch(paymentActions.setDaimoError(null)) - }, [dispatch, recipient]) - - useEffect(() => { - // Skip balance check if on CONFIRM or STATUS view, or if transaction is being processed, or if we have pending txs - if (currentView === 'CONFIRM' || currentView === 'STATUS' || isProcessing || hasPendingTransactions) { - return - } - - dispatch(paymentActions.setError(null)) - - const currentInputAmountStr = String(inputTokenAmount) - const parsedInputAmount = parseFloat(currentInputAmountStr.replace(/,/g, '')) - - if (!currentInputAmountStr || isNaN(parsedInputAmount) || parsedInputAmount <= 0) { - // if input is invalid or zero, no balance check is needed yet, or clear error if it was for insufficient balance - return - } - - try { - if (isExternalWalletFlow) { - // ADD MONEY FLOW: Strictly check external wallet if connected - if (isExternalWalletConnected && selectedTokenData && selectedTokenBalance !== undefined) { - if (selectedTokenData.decimals === undefined) { - console.error('Selected token has no decimals information for Add Money.') - dispatch(paymentActions.setError('Cannot verify balance: token data incomplete.')) - return - } - const numericSelectedTokenBalance = parseFloat(String(selectedTokenBalance).replace(/,/g, '')) - if (numericSelectedTokenBalance < parsedInputAmount) { - dispatch(paymentActions.setError('Insufficient balance in connected wallet')) - } else { - dispatch(paymentActions.setError(null)) - } - } else { - // not connected or data missing for add money flow, clear error - dispatch(paymentActions.setError(null)) - } - } else { - // regular send/pay - const isExternalRecipient = recipient?.recipientType === 'ADDRESS' || recipient?.recipientType === 'ENS' - - if ( - !showRequestPotInitialView && // don't apply balance check on request pot payment initial view - isActivePeanutWallet && - !isExternalRecipient - ) { - // peanut wallet payment for USERNAME recipients - const walletNumeric = parseFloat(String(peanutWalletBalance).replace(/,/g, '')) - if (walletNumeric < parsedInputAmount) { - dispatch(paymentActions.setError('Insufficient balance')) - } else { - dispatch(paymentActions.setError(null)) - } - } else if ( - !showRequestPotInitialView && - isActivePeanutWallet && - isExternalRecipient && - areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN) - ) { - // for external recipients (ADDRESS/ENS) paying with peanut wallet, check peanut wallet balance directly - const walletNumeric = parseFloat(String(peanutWalletBalance).replace(/,/g, '')) - if (walletNumeric < parsedInputAmount) { - dispatch(paymentActions.setError('Insufficient balance')) - } else { - dispatch(paymentActions.setError(null)) - } - } else if ( - isExternalWalletConnected && - !isActivePeanutWallet && - selectedTokenData && - selectedTokenBalance !== undefined - ) { - // external wallet payment (not add money flow) - if (selectedTokenData.decimals === undefined) { - console.error('Selected token has no decimals information.') - dispatch(paymentActions.setError('Cannot verify balance: token data incomplete.')) - return - } - const numericSelectedTokenBalance = parseFloat(String(selectedTokenBalance).replace(/,/g, '')) - if (numericSelectedTokenBalance < parsedInputAmount) { - dispatch(paymentActions.setError('Insufficient balance')) - } else { - dispatch(paymentActions.setError(null)) - } - } else if (isExternalRecipient && isActivePeanutWallet) { - // for external recipients with peanut wallet using non-USDC tokens, balance will be checked via cross-chain route - dispatch(paymentActions.setError(null)) - } else { - dispatch(paymentActions.setError(null)) - } - } - } catch (e) { - console.error('Error during balance check:', e) - if ( - e instanceof Error && - (e.message.toLowerCase().includes('invalid character') || - e.message.toLowerCase().includes('invalid BigInt value')) - ) { - dispatch(paymentActions.setError('Invalid amount for balance check')) - } else { - dispatch(paymentActions.setError('Error verifying balance')) - } - } - }, [ - selectedTokenBalance, - peanutWalletBalance, - selectedTokenAddress, - inputTokenAmount, - isActivePeanutWallet, - dispatch, - selectedTokenData, - isExternalWalletConnected, - isExternalWalletFlow, - showRequestPotInitialView, - currentView, - isProcessing, - hasPendingTransactions, - recipient?.recipientType, - ]) - - // Calculate USD value when requested token price is available - useEffect(() => { - // skip this step for request pot payments - // Amount is set by the user so we dont need to manually update it - // No usd conversion needed because amount will always be USDC - if ( - showRequestPotInitialView || - !requestedTokenPriceData?.price || - !requestDetails?.tokenAmount || - isRequestPotLink - ) - return - - const tokenAmount = parseFloat(requestDetails.tokenAmount) - if (isNaN(tokenAmount) || tokenAmount <= 0) return - - if (isNaN(requestedTokenPriceData.price) || requestedTokenPriceData.price === 0) return - - const usdValue = formatAmount(tokenAmount * requestedTokenPriceData.price) - - setInputTokenAmount(usdValue) - setUsdValue(usdValue) - }, [requestedTokenPriceData?.price, requestDetails?.tokenAmount, showRequestPotInitialView, isRequestPotLink]) - - const canInitiatePayment = useMemo(() => { - let amountIsSet = false - if (isActivePeanutWallet) { - amountIsSet = !!inputTokenAmount && parseFloat(inputTokenAmount) > 0 - } else { - amountIsSet = - (!!inputTokenAmount && parseFloat(inputTokenAmount) > 0) || (!!usdValue && parseFloat(usdValue) > 0) - } - - const isExternalRecipient = recipient?.recipientType === 'ADDRESS' || recipient?.recipientType === 'ENS' - // for external recipients, token selection is required - // for USERNAME recipients, token is always PEANUT_WALLET_TOKEN - const tokenSelected = isExternalRecipient - ? !!selectedTokenAddress && !!selectedChainID - : !!selectedTokenAddress && !!selectedChainID - const recipientExists = !!recipient - const walletConnected = isConnected - - // If its requestPotPayment, we only need to check if the recipient exists, amount is set, and token is selected - if (showRequestPotInitialView) { - return recipientExists && amountIsSet && tokenSelected && showRequestPotInitialView - } - - return recipientExists && amountIsSet && tokenSelected && walletConnected - }, [ - showRequestPotInitialView, - recipient, - inputTokenAmount, - usdValue, - selectedTokenAddress, - selectedChainID, - isConnected, - isActivePeanutWallet, - ]) - - const handleAcceptInvite = async () => { - try { - setIsAcceptingInvite(true) - const inviteCode = `${recipient?.identifier}INVITESYOU` - const result = await invitesApi.acceptInvite(inviteCode, EInviteType.PAYMENT_LINK) - - if (!result.success) { - console.error('Failed to accept invite') - setInviteError(true) - setIsAcceptingInvite(false) - return false - } - - // fetch user so that we have the latest state and user can access the app. - // We dont need to wait for this, can happen in background. - await fetchUser() - setIsAcceptingInvite(false) - return true - } catch (error) { - console.error('Failed to accept invite', error) - setInviteError(true) - setIsAcceptingInvite(false) - return false - } - } - - const handleInitiatePayment = useCallback(async () => { - // clear invite error - if (inviteError) { - setInviteError(false) - } - // redirect to add money if insufficient balance - if (!showRequestPotInitialView && isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) { - // if the user doesn't have app access, accept the invite before redirecting - // only applies to USERNAME recipients (invite links) - if (recipient.recipientType === 'USERNAME' && !user?.user.hasAppAccess) { - const isAccepted = await handleAcceptInvite() - if (!isAccepted) return - } - router.push('/add-money') - return - } - - // skip this step for request pots initial view - if (!showRequestPotInitialView && !isExternalWalletConnected && isExternalWalletFlow) { - try { - await initializeAppKit() - openReownModal() - } catch (error) { - console.error('Failed to initialize AppKit:', error) - Sentry.captureException(error, { - tags: { context: 'payment_form_external_wallet' }, - extra: { flow: 'external_wallet_payment' }, - }) - } - } - - // skip this step for request pots initial view - if (!showRequestPotInitialView && !isConnected) { - dispatch(walletActions.setSignInModalVisible(true)) - return - } - - if (!canInitiatePayment) return - - // regular payment flow - if (!inputTokenAmount || parseFloat(inputTokenAmount) <= 0) { - console.error('Invalid amount entered') - dispatch(paymentActions.setError('Please enter a valid amount')) - return - } - - if (inputUsdValue && parseFloat(inputUsdValue) > 0) { - dispatch(paymentActions.setUsdAmount(inputUsdValue)) - } - - if ( - !isActivePeanutWallet && - isExternalWalletConnected && - selectedTokenData && - selectedChainID && - !!chargeDetails - ) { - dispatch(paymentActions.setView('CONFIRM')) - return - } - - dispatch(paymentActions.setError(null)) - - const requestedToken = chargeDetails?.tokenAddress ?? requestDetails?.tokenAddress - const requestedChain = chargeDetails?.chainId ?? requestDetails?.chainId - - let tokenAmount = inputTokenAmount - if ( - requestedToken && - requestedTokenPriceData?.price && - (requestedChain !== selectedChainID || !areEvmAddressesEqual(requestedToken, selectedTokenAddress)) - ) { - // Validate price before division - if (isNaN(requestedTokenPriceData.price) || requestedTokenPriceData.price === 0) { - console.error('Invalid token price for conversion') - dispatch(paymentActions.setError('Cannot calculate token amount: invalid price data')) - return - } - - const usdAmount = parseFloat(inputUsdValue) - if (isNaN(usdAmount)) { - console.error('Invalid USD amount') - dispatch(paymentActions.setError('Invalid amount entered')) - return - } - - tokenAmount = (usdAmount / requestedTokenPriceData.price).toString() - } - - const payload: InitiatePaymentPayload = { - recipient: recipient, - tokenAmount, - requestId: requestId ?? undefined, - chargeId: chargeDetails?.uuid, - currency, - currencyAmount, - isExternalWalletFlow: !!isExternalWalletFlow, - transactionType: isExternalWalletFlow - ? 'DEPOSIT' - : isDirectUsdPayment || !requestId - ? 'DIRECT_SEND' - : 'REQUEST', - attachmentOptions: attachmentOptions, - returnAfterChargeCreation: !!showRequestPotInitialView, // For request pot initial view, return after charge creation without initiating payment - } - - console.log('Initiating payment with payload:', payload) - - const result = await initiatePayment(payload) - - if (result.status === 'Success') { - triggerHaptic() - dispatch(paymentActions.setView('STATUS')) - } else if (result.status === 'Charge Created') { - if (!showRequestPotInitialView) { - dispatch(paymentActions.setView('CONFIRM')) - } - } else if (result.status === 'Error') { - console.error('Payment initiation failed:', result.error) - } else { - console.warn('Unexpected status from usePaymentInitiator:', result.status) - } - }, [ - canInitiatePayment, - isDepositRequest, - isConnected, - openReownModal, - recipient, - inputTokenAmount, - requestId, - initiatePayment, - chargeDetails, - isExternalWalletFlow, - requestDetails, - selectedTokenAddress, - selectedChainID, - inputUsdValue, - requestedTokenPriceData?.price, - inviteError, - handleAcceptInvite, - showRequestPotInitialView, - ]) - - const getButtonText = () => { - if (!isExternalWalletConnected && isExternalWalletFlow) { - return 'Connect Wallet' - } - - if (isExternalWalletFlow) { - return 'Review' - } - - if (isProcessing) { - return 'Send' - } - - if (showRequestPotInitialView) { - return 'Choose payment method' - } - - if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) { - return ( -
-
Add funds to
-
- Peanut Logo - Peanut Logo -
-
- ) - } - - if (isActivePeanutWallet) { - return ( -
-
Send with
-
- Peanut Logo - Peanut Logo -
-
- ) - } - - return 'Review' - } - - const getButtonIcon = (): IconName | undefined => { - if (!showRequestPotInitialView && !isExternalWalletConnected && isExternalWalletFlow) return 'wallet-outline' - - if (!showRequestPotInitialView && isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) - return 'arrow-down' - - if (!showRequestPotInitialView && !isProcessing && isActivePeanutWallet && !isExternalWalletFlow) - return 'arrow-up-right' - - return undefined - } - - useEffect(() => { - if (!inputTokenAmount) return - if (selectedTokenData?.price) { - const amount = parseFloat(inputTokenAmount) - if (isNaN(amount) || amount < 0) return - - if (isNaN(selectedTokenData.price) || selectedTokenData.price === 0) return - - setUsdValue((amount * selectedTokenData.price).toString()) - } - }, [inputTokenAmount, selectedTokenData?.price]) - - // Initialize inputTokenAmount - useEffect(() => { - // skip this step for request pot payments and request pot links (charge view) - // Amount is set by the user so we dont need to manually update it - if (amount && !inputTokenAmount && !initialSetupDone && !showRequestPotInitialView && !isRequestPotLink) { - setInputTokenAmount(amount) - } - }, [amount, inputTokenAmount, initialSetupDone, showRequestPotInitialView, isRequestPotLink]) - - // Trigger payment with peanut from action list - useEffect(() => { - if (triggerPayWithPeanut) { - handleInitiatePayment() - setTriggerPayWithPeanut(false) - } - }, [triggerPayWithPeanut, handleInitiatePayment, setTriggerPayWithPeanut]) - - const isInsufficientBalanceError = useMemo(() => { - return error?.includes("You don't have enough balance.") || error?.includes('Insufficient balance') - }, [error]) - - const isButtonDisabled = useMemo(() => { - if (isProcessing) return true - if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) return false - if (!!error) return true - - // ensure inputTokenAmount is a valid positive number before allowing payment - const numericAmount = parseFloat(inputTokenAmount) - - if (isNaN(numericAmount) || numericAmount <= 0) { - if (!isExternalWalletFlow) return true - } - - if (isExternalWalletFlow) { - if (!isExternalWalletConnected) return false // "Connect Wallet" button should be active - return ( - !inputTokenAmount || - isNaN(parseFloat(inputTokenAmount)) || - parseFloat(inputTokenAmount) <= 0 || - !selectedTokenAddress || - !selectedChainID || - isProcessing - ) - } - - if (flow === 'request_pay') return false - - // fallback for other cases if not explicitly handled above - return false - }, [ - isProcessing, - error, - inputTokenAmount, - isExternalWalletFlow, - isExternalWalletConnected, - selectedTokenAddress, - selectedChainID, - isConnected, - isActivePeanutWallet, - ]) - - const recipientDisplayName = useMemo(() => { - return recipient ? recipient.identifier : 'Unknown Recipient' - }, [recipient]) - - const handleGoBack = () => { - if (window.history.length > 1) { - router.back() - } else { - router.push('/') - } - } - - const contributors = getContributorsFromCharge(requestDetails?.charges || []) - - const totalAmountCollected = requestDetails?.totalCollectedAmount ?? 0 - - // determine when to use TokenAmountInput vs PaymentAmountInput - // use TokenAmountInput for: - // 1. request pot payments to usernames (typing bug fix) - // 2. payments to ADDRESS/ENS recipients (typing bug fix) - // note: kush to kill the annoying token amount input component - const shouldUseTokenAmountInput = useMemo(() => { - const isRequestPotToUsername = - showRequestPotInitialView && recipient?.recipientType === 'USERNAME' && !!requestDetails - const isExternalRecipient = recipient?.recipientType === 'ADDRESS' || recipient?.recipientType === 'ENS' - return isRequestPotToUsername || isExternalRecipient - }, [showRequestPotInitialView, recipient?.recipientType, requestDetails]) - - const defaultSliderValue = useMemo(() => { - const charges = requestDetails?.charges - const totalAmount = requestDetails?.tokenAmount ? parseFloat(requestDetails.tokenAmount) : 0 - const totalCollected = totalAmountCollected - - if (totalAmount <= 0) return { percentage: 0, suggestedAmount: 0 } - - // No charges yet - suggest 100% (full pot) - if (!charges || charges.length === 0) { - return { percentage: 100, suggestedAmount: totalAmount } - } - - // Calculate average contribution from existing charges - const contributionAmounts = charges - .map((charge) => parseFloat(charge.tokenAmount)) - .filter((amount) => !isNaN(amount) && amount > 0) - - if (contributionAmounts.length === 0) return { percentage: 0, suggestedAmount: 0 } - - // Check if this is an equal-split pattern (1 payment at ~33% or 2 payments at ~66%) - const collectedPercentage = (totalCollected / totalAmount) * 100 - const isOneThirdCollected = Math.abs(collectedPercentage - 100 / 3) < 2 // ~33.33% - const isTwoThirdsCollected = Math.abs(collectedPercentage - 200 / 3) < 2 // ~66.67% - - if (isOneThirdCollected || isTwoThirdsCollected) { - // Suggest exact 33.33% to maintain equal split pattern - const exactThird = 100 / 3 - return { percentage: exactThird, suggestedAmount: totalAmount * (exactThird / 100) } - } - - // Otherwise suggest the median contribution (more robust against outliers than average) - const sortedAmounts = [...contributionAmounts].sort((a, b) => a - b) - const midIndex = Math.floor(sortedAmounts.length / 2) - const suggestedAmount = - sortedAmounts.length % 2 === 0 - ? (sortedAmounts[midIndex - 1] + sortedAmounts[midIndex]) / 2 // even: average of middle two - : sortedAmounts[midIndex] // odd: middle value - - // Convert amount to percentage of total pot - const percentage = (suggestedAmount / totalAmount) * 100 - // Cap at 100% max - return { percentage: Math.min(percentage, 100), suggestedAmount } - }, [requestDetails?.charges, requestDetails?.tokenAmount, totalAmountCollected]) - - return ( -
- -
- {isExternalWalletConnected && isUsingExternalWallet && ( - - )} - {/* Recipient Info Card */} - {recipient && !isExternalWalletFlow && ( - - )} - - {/* mark the date - 16/11/2025, the author has written worse piece of code that the humanity will ever witness, but it works, so in the promiseland of post devconnect, i the kush, the author will kill this code to fix it once and for all */} - {/* Amount Display Card */} - {/* use TokenAmountInput for direct usd payments, request pot payments, and external address payments to avoid typing issues */} - {isDirectUsdPayment || shouldUseTokenAmountInput ? ( - setInputTokenAmount(value || '')} - setUsdValue={(value: string) => { - setInputUsdValue(value) - dispatch(paymentActions.setUsdAmount(value)) - }} - setCurrencyAmount={setCurrencyAmount} - className="w-full" - disabled={ - !showRequestPotInitialView && - !isExternalWalletFlow && - (!!requestDetails?.tokenAmount || !!chargeDetails?.tokenAmount) - } - walletBalance={isActivePeanutWallet ? peanutWalletBalance : undefined} - currency={currency} - hideCurrencyToggle={!currency} - hideBalance={isExternalWalletFlow} - showSlider={showRequestPotInitialView && amount ? Number(amount) > 0 : false} - maxAmount={showRequestPotInitialView && amount ? Number(amount) : undefined} - amountCollected={showRequestPotInitialView ? totalAmountCollected : 0} - defaultSliderValue={showRequestPotInitialView ? defaultSliderValue.percentage : undefined} - defaultSliderSuggestedAmount={ - showRequestPotInitialView ? defaultSliderValue.suggestedAmount : undefined - } - /> - ) : ( - setInputTokenAmount(value || '')} - setUsdValue={(value: string) => { - setInputUsdValue(value) - dispatch(paymentActions.setUsdAmount(value)) - }} - setCurrencyAmount={setCurrencyAmount} - className="w-full" - disabled={ - !showRequestPotInitialView && - !isExternalWalletFlow && - (!!requestDetails?.tokenAmount || !!chargeDetails?.tokenAmount) - } - walletBalance={isActivePeanutWallet ? peanutWalletBalance : undefined} - currency={currency} - hideCurrencyToggle={!currency} - hideBalance={isExternalWalletFlow} - showSlider={showRequestPotInitialView && amount ? Number(amount) > 0 : false} - maxAmount={showRequestPotInitialView && amount ? Number(amount) : undefined} - amountCollected={showRequestPotInitialView ? totalAmountCollected : 0} - defaultSliderValue={showRequestPotInitialView ? defaultSliderValue.percentage : undefined} - defaultSliderSuggestedAmount={ - showRequestPotInitialView ? defaultSliderValue.suggestedAmount : undefined - } - /> - )} - - {/* Token selector for external ADDRESS/ENS recipients */} - {/* only show if chain is not specified in URL */} - {!isExternalWalletFlow && - !showRequestPotInitialView && - !chain?.chainId && - (recipient?.recipientType === 'ADDRESS' || recipient?.recipientType === 'ENS') && - isConnected && ( -
- - {selectedTokenAddress && - selectedChainID && - !( - areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN) && - selectedChainID === PEANUT_WALLET_CHAIN.id.toString() - ) && ( -
- Use USDC on Arbitrum for free transactions! -
- )} -
- )} - - {isDirectUsdPayment && ( - dispatch(paymentActions.setAttachmentOptions(options))} - className="h-11" - /> - )} - -
- {(showRequestPotInitialView || - (isPeanutWalletConnected && (!error || isInsufficientBalanceError))) && ( - - )} - {isPeanutWalletConnected && error && !isInsufficientBalanceError && ( - - )} - {daimoError && } - - {!daimoError && error && ( - - )} -
-
- - {showRequestPotInitialView && contributors.length > 0 && ( -
-

Contributors ({contributors.length})

- {contributors.map((contributor, index) => ( - - ))} -
- )} - - setDisconnectWagmiModal(false)} - title="Disconnect wallet?" - description="You'll need to reconnect to continue using crypto features." - icon="switch" - ctaClassName="flex-row" - hideModalCloseButton={true} - ctas={[ - { - text: 'Disconnect', - onClick: () => { - disconnectWagmi() - setDisconnectWagmiModal(false) - }, - shadowSize: '4', - }, - { - text: 'Cancel', - onClick: () => { - setDisconnectWagmiModal(false) - }, - shadowSize: '4', - className: 'bg-grey-4 hover:bg-grey-4 hover:text-black active:bg-grey-4', - }, - ]} - /> -
- ) -} diff --git a/src/components/Payment/PaymentInfoRow.tsx b/src/components/Payment/PaymentInfoRow.tsx index 19962b8cb..0e9cebade 100644 --- a/src/components/Payment/PaymentInfoRow.tsx +++ b/src/components/Payment/PaymentInfoRow.tsx @@ -51,7 +51,8 @@ export const PaymentInfoRow = ({ > setShowMoreInfo(!showMoreInfo)} /> {showMoreInfo && ( diff --git a/src/components/Payment/Views/Confirm.payment.view.tsx b/src/components/Payment/Views/Confirm.payment.view.tsx deleted file mode 100644 index 786e8952a..000000000 --- a/src/components/Payment/Views/Confirm.payment.view.tsx +++ /dev/null @@ -1,566 +0,0 @@ -'use client' - -import { Button } from '@/components/0_Bruddle' -import ActionModal from '@/components/Global/ActionModal' -import Card from '@/components/Global/Card' -import DisplayIcon from '@/components/Global/DisplayIcon' -import ErrorAlert from '@/components/Global/ErrorAlert' -import { type IconName } from '@/components/Global/Icons/Icon' -import NavHeader from '@/components/Global/NavHeader' -import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard' -import PeanutLoading from '@/components/Global/PeanutLoading' -import { TRANSACTIONS } from '@/constants/query.consts' -import { tokenSelectorContext } from '@/context' -import { usePaymentInitiator } from '@/hooks/usePaymentInitiator' -import { useTokenChainIcons } from '@/hooks/useTokenChainIcons' -import { useWallet } from '@/hooks/wallet/useWallet' -import { useAppDispatch, usePaymentStore } from '@/redux/hooks' -import { paymentActions } from '@/redux/slices/payment-slice' -import { chargesApi } from '@/services/charges' -import { ErrorHandler, formatAmount, areEvmAddressesEqual, isStableCoin } from '@/utils' -import { useQueryClient } from '@tanstack/react-query' -import { useSearchParams } from 'next/navigation' -import { useCallback, useContext, useEffect, useMemo, useState } from 'react' -import { useAccount } from 'wagmi' -import { PaymentInfoRow } from '../PaymentInfoRow' -import { formatUnits } from 'viem' -import type { Address } from 'viem' -import { - PEANUT_WALLET_CHAIN, - PEANUT_WALLET_TOKEN, - ROUTE_NOT_FOUND_ERROR, - PEANUT_WALLET_TOKEN_SYMBOL, -} from '@/constants' -import { captureMessage } from '@sentry/nextjs' -import AddressLink from '@/components/Global/AddressLink' -import { useHaptic } from 'use-haptic' - -type ConfirmPaymentViewProps = { - currency?: { - code: string - symbol: string - price: number - } - currencyAmount?: string - isExternalWalletFlow?: boolean - /** Whether this is a direct payment, for xchain we dont care if a little - * less arrives*/ - isDirectUsdPayment?: boolean - headerTitle?: string -} - -/** - * Confirmation view for payment transactions. Displays payment details, fees, and handles - * transaction execution for various payment flows including cross-chain payments, direct USD - * payments, and add money flows. - * - * @param currency - Currency details for display (code, symbol, price) - * @param currencyAmount - Amount in the specified currency - * @param isExternalWalletFlow - Whether this is an add money flow (deposit to wallet) - * @param isDirectUsdPayment - Whether this is a direct payment, for xchain we dont care if a little less arrives - */ -export default function ConfirmPaymentView({ - currency, - currencyAmount, - isExternalWalletFlow, - isDirectUsdPayment = false, - headerTitle, -}: ConfirmPaymentViewProps) { - const dispatch = useAppDispatch() - const searchParams = useSearchParams() - const chargeIdFromUrl = searchParams.get('chargeId') - const { chargeDetails, parsedPaymentData, usdAmount } = usePaymentStore() - const { - initiatePayment, - prepareTransactionDetails, - isProcessing, - isPreparingTx, - loadingStep, - error: paymentError, - estimatedGasCostUsd, - isCalculatingFees, - isEstimatingGas, - isFeeEstimationError, - cancelOperation: cancelPaymentOperation, - xChainRoute, - } = usePaymentInitiator() - const { selectedTokenData, selectedChainID } = useContext(tokenSelectorContext) - const { isConnected: isPeanutWallet, fetchBalance, address: peanutWalletAddress } = useWallet() - const { isConnected: isWagmiConnected, address: wagmiAddress } = useAccount() - const queryClient = useQueryClient() - const [isRouteExpired, setIsRouteExpired] = useState(false) - const { triggerHaptic } = useHaptic() - - const isUsingExternalWallet = useMemo( - () => isExternalWalletFlow || !isPeanutWallet, - [isExternalWalletFlow, isPeanutWallet] - ) - - const networkFee = useMemo(() => { - if (isFeeEstimationError) return '-' - if (estimatedGasCostUsd === undefined) { - return isUsingExternalWallet ? '-' : 'Sponsored by Peanut!' - } - - // external wallet flows - if (isUsingExternalWallet) { - return estimatedGasCostUsd < 0.01 ? '$ <0.01' : `$ ${estimatedGasCostUsd.toFixed(2)}` - } - - // peanut-sponsored transactions - if (estimatedGasCostUsd < 0.01) return 'Sponsored by Peanut!' - - return ( - <> - $ {estimatedGasCostUsd.toFixed(2)} - {' - '} - Sponsored by Peanut! - - ) - }, [estimatedGasCostUsd, isFeeEstimationError, isUsingExternalWallet]) - - const { - tokenIconUrl: sendingTokenIconUrl, - chainIconUrl: sendingChainIconUrl, - resolvedChainName: sendingResolvedChainName, - resolvedTokenSymbol: sendingResolvedTokenSymbol, - } = useTokenChainIcons({ - chainId: isUsingExternalWallet ? selectedChainID : PEANUT_WALLET_CHAIN.id.toString(), - tokenAddress: isUsingExternalWallet ? selectedTokenData?.address : PEANUT_WALLET_TOKEN, - tokenSymbol: isUsingExternalWallet ? selectedTokenData?.symbol : PEANUT_WALLET_TOKEN_SYMBOL, - }) - - const { - tokenIconUrl: requestedTokenIconUrl, - chainIconUrl: requestedChainIconUrl, - resolvedChainName: requestedResolvedChainName, - resolvedTokenSymbol: requestedResolvedTokenSymbol, - } = useTokenChainIcons({ - chainId: chargeDetails?.chainId, - tokenAddress: chargeDetails?.tokenAddress, - tokenSymbol: chargeDetails?.tokenSymbol, - }) - - const showExternalWalletConfirmationModal = useMemo((): boolean => { - if (isCalculatingFees || isEstimatingGas) return false - - return ( - isProcessing && - isUsingExternalWallet && - ['Switching Network', 'Sending Transaction', 'Confirming Transaction', 'Preparing Transaction'].includes( - loadingStep - ) - ) - }, [isProcessing, loadingStep, isCalculatingFees, isEstimatingGas]) - - useEffect(() => { - if (chargeIdFromUrl && !chargeDetails) { - chargesApi - .get(chargeIdFromUrl) - .then((fetchedChargeDetails) => { - dispatch(paymentActions.setChargeDetails(fetchedChargeDetails)) - }) - .catch((error) => { - const errorString = ErrorHandler(error) - dispatch(paymentActions.setError(errorString)) - dispatch(paymentActions.setChargeDetails(null)) - }) - } else if (!chargeIdFromUrl && !chargeDetails) { - dispatch( - paymentActions.setError('Payment details are missing. Please go back and try again or contact support.') - ) - } - }, [chargeIdFromUrl, chargeDetails, dispatch]) - - const handleRouteRefresh = useCallback(async () => { - if (chargeDetails && selectedTokenData && selectedChainID) { - setIsRouteExpired(false) - const fromTokenAddress = !isUsingExternalWallet ? PEANUT_WALLET_TOKEN : selectedTokenData.address - const fromChainId = !isUsingExternalWallet ? PEANUT_WALLET_CHAIN.id.toString() : selectedChainID - const usdAmount = - isDirectUsdPayment && chargeDetails.currencyCode.toLowerCase() === 'usd' - ? chargeDetails.currencyAmount - : undefined - const senderAddress = isUsingExternalWallet ? wagmiAddress : peanutWalletAddress - await prepareTransactionDetails({ - chargeDetails, - from: { - address: senderAddress as Address, - tokenAddress: fromTokenAddress as Address, - chainId: fromChainId, - }, - usdAmount, - disableCoral: isExternalWalletFlow && isUsingExternalWallet, - }) - } - }, [ - chargeDetails, - selectedTokenData, - selectedChainID, - prepareTransactionDetails, - isDirectUsdPayment, - wagmiAddress, - peanutWalletAddress, - isExternalWalletFlow, - isUsingExternalWallet, - ]) - - useEffect(() => { - // get route on mount - handleRouteRefresh() - }, [handleRouteRefresh]) - - const isConnected = useMemo(() => isPeanutWallet || isWagmiConnected, [isPeanutWallet, isWagmiConnected]) - - const isLoading = useMemo( - () => isProcessing || isPreparingTx || isCalculatingFees || isEstimatingGas, - [isProcessing, isPreparingTx, isCalculatingFees, isEstimatingGas] - ) - - const isCrossChainPayment = useMemo((): boolean => { - if (!chargeDetails) return false - if (!isUsingExternalWallet) { - return ( - !areEvmAddressesEqual(chargeDetails.tokenAddress, PEANUT_WALLET_TOKEN) || - chargeDetails.chainId !== PEANUT_WALLET_CHAIN.id.toString() - ) - } else if (selectedTokenData && selectedChainID) { - return ( - !areEvmAddressesEqual(chargeDetails.tokenAddress, selectedTokenData.address) || - chargeDetails.chainId !== selectedChainID - ) - } - return false - }, [chargeDetails, selectedTokenData, selectedChainID]) - - const routeTypeError = useMemo((): string | null => { - // error only applies to peanut wallet flows (not external wallet) where cross-chain swap route is RFQ-required. - if (!isCrossChainPayment || !xChainRoute || isUsingExternalWallet || !isPeanutWallet) return null - - // For peanut wallet flows, only RFQ routes are allowed - if (xChainRoute.type === 'swap') { - captureMessage('No RFQ route found for this token pair', { - level: 'warning', - extra: { - flow: 'payment', - routeObject: xChainRoute, - }, - }) - return ROUTE_NOT_FOUND_ERROR - } - - return null - }, [isCrossChainPayment, xChainRoute, isUsingExternalWallet, isPeanutWallet]) - - const errorMessage = useMemo((): string | undefined => { - if (isRouteExpired) return 'This quoute has expired. Please retry to fetch latest quote.' - return routeTypeError ?? paymentError ?? undefined - }, [routeTypeError, paymentError, isRouteExpired]) - - const handleGoBack = () => { - if (isExternalWalletFlow) { - dispatch(paymentActions.setView('INITIAL')) - return - } - dispatch(paymentActions.setView('INITIAL')) - window.history.replaceState(null, '', `${window.location.pathname}`) - dispatch(paymentActions.setChargeDetails(null)) - dispatch(paymentActions.setError(null)) - } - - const handlePayment = useCallback(async () => { - if (!chargeDetails || !parsedPaymentData) return - - const result = await initiatePayment({ - recipient: parsedPaymentData.recipient, - tokenAmount: chargeDetails.tokenAmount, - chargeId: chargeDetails.uuid, - skipChargeCreation: true, - currency, - currencyAmount, - isExternalWalletFlow, - transactionType: isExternalWalletFlow ? 'DEPOSIT' : 'REQUEST', - }) - - if (result.success) { - setTimeout(() => { - fetchBalance() - queryClient.invalidateQueries({ queryKey: [TRANSACTIONS] }) - }, 3000) - triggerHaptic() - dispatch(paymentActions.setView('STATUS')) - } - }, [ - chargeDetails, - initiatePayment, - parsedPaymentData, - dispatch, - fetchBalance, - queryClient, - currency, - currencyAmount, - isExternalWalletFlow, - ]) - - const handleRetry = useCallback(async () => { - if (routeTypeError) { - handleGoBack() - } else if (isRouteExpired) { - await handleRouteRefresh() - } else { - await handlePayment() - } - }, [handlePayment, routeTypeError, handleRouteRefresh, isRouteExpired]) - - const getButtonText = useCallback(() => { - const buttonText = headerTitle ? 'Send money' : 'Add Money' - if (isProcessing) { - if (isExternalWalletFlow) return buttonText - return loadingStep === 'Idle' ? 'Send' : 'Sending' - } - if (isExternalWalletFlow) return buttonText - if (isEstimatingGas || isCalculatingFees || isPreparingTx) return 'Send' - return 'Send' - }, [isProcessing, loadingStep, isPreparingTx, isEstimatingGas, isCalculatingFees, isExternalWalletFlow]) - - const getIcon = useCallback((): IconName | undefined => { - if (isExternalWalletFlow && headerTitle) return 'arrow-up-right' - if (isExternalWalletFlow) return 'arrow-down' - if (isProcessing) return undefined - return 'arrow-up-right' - }, [isExternalWalletFlow]) - - const amountForDisplay = useMemo(() => { - if (usdAmount) { - return `$ ${formatAmount(Number(usdAmount))}` - } - return formatAmount(chargeDetails?.tokenAmount ?? '') - }, [usdAmount, chargeDetails?.tokenAmount]) - - const symbolForDisplay = useMemo(() => { - if (usdAmount) { - return '' - } - return chargeDetails?.tokenSymbol ?? '' - }, [usdAmount, chargeDetails?.tokenSymbol]) - - if (!chargeDetails && !paymentError) { - return chargeIdFromUrl ? : null - } - - if (!chargeDetails && paymentError) { - const message = paymentError - return ( -
- - -
- ) - } - - const minReceived = useMemo(() => { - if (!chargeDetails?.tokenDecimals || !requestedResolvedTokenSymbol) return null - if (!xChainRoute) { - return `$ ${chargeDetails?.tokenAmount}` - } - const amount = formatAmount( - formatUnits(BigInt(xChainRoute.rawResponse.route.estimate.toAmountMin), chargeDetails.tokenDecimals) - ) - return isStableCoin(requestedResolvedTokenSymbol) ? `$ ${amount}` : `${amount} ${requestedResolvedTokenSymbol}` - }, [xChainRoute, chargeDetails?.tokenDecimals, requestedResolvedTokenSymbol]) - - return ( -
- -
- {parsedPaymentData?.recipient && ( - { - setIsRouteExpired(true) - }} - disableTimerRefetch={isProcessing} - timerError={routeTypeError} - /> - )} - - - - {isCrossChainPayment && !isExternalWalletFlow && ( - - } - /> - )} - - } - /> - - {isExternalWalletFlow && ( - - } - /> - )} - - - - - - -
- {errorMessage ? ( - - ) : ( - - )} - {errorMessage && ( -
- -
- )} -
- { - cancelPaymentOperation() - }} - title="Continue in your wallet" - description="Please confirm the transaction in your wallet app to proceed." - isLoadingIcon={true} - preventClose={true} - /> -
-
- ) -} - -interface TokenChainInfoDisplayProps { - tokenIconUrl?: string - chainIconUrl?: string - resolvedTokenSymbol?: string - fallbackTokenSymbol: string - resolvedChainName?: string - fallbackChainName: string -} - -/** - * Displays token and chain information with icons and names. - * Shows token icon with chain icon as a badge overlay, along with formatted text. - * - * @param tokenIconUrl - URL for the token icon - * @param chainIconUrl - URL for the chain icon (displayed as overlay) - * @param resolvedTokenSymbol - Resolved token symbol from API - * @param fallbackTokenSymbol - Fallback token symbol if resolution fails - * @param resolvedChainName - Resolved chain name from API - * @param fallbackChainName - Fallback chain name if resolution fails - */ -function TokenChainInfoDisplay({ - tokenIconUrl, - chainIconUrl, - resolvedTokenSymbol, - fallbackTokenSymbol, - resolvedChainName, - fallbackChainName, -}: TokenChainInfoDisplayProps) { - const tokenSymbol = resolvedTokenSymbol || fallbackTokenSymbol - const chainName = resolvedChainName || fallbackChainName - - return ( -
- {(tokenIconUrl || chainIconUrl) && ( -
- {tokenIconUrl && ( - - )} - {chainIconUrl && ( -
- -
- )} -
- )} - - {tokenSymbol} on {chainName} - -
- ) -} diff --git a/src/components/Payment/Views/Error.validation.view.tsx b/src/components/Payment/Views/Error.validation.view.tsx index 48c140354..a9d8502fd 100644 --- a/src/components/Payment/Views/Error.validation.view.tsx +++ b/src/components/Payment/Views/Error.validation.view.tsx @@ -1,11 +1,11 @@ 'use client' import PEANUTMAN_CRY from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_05.gif' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import Link from 'next/link' import Image from 'next/image' import { useRouter } from 'next/navigation' -import { useSupportModalContext } from '@/context/SupportModalContext' +import { useModalsContext } from '@/context/ModalsContext' import { useEffect, useState } from 'react' export type ValidationErrorViewProps = { @@ -28,7 +28,7 @@ function ValidationErrorView({ supportButtonText = 'Talk to support', }: ValidationErrorViewProps) { const router = useRouter() - const { openSupportWithMessage } = useSupportModalContext() + const { openSupportWithMessage } = useModalsContext() const [currentUrl, setCurrentUrl] = useState('') useEffect(() => { diff --git a/src/components/Payment/Views/Initial.payment.view.tsx b/src/components/Payment/Views/Initial.payment.view.tsx deleted file mode 100644 index 09e3b16a5..000000000 --- a/src/components/Payment/Views/Initial.payment.view.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { PaymentForm, type PaymentFormProps } from '../PaymentForm' - -export default function InitialPaymentView(props: PaymentFormProps) { - return ( - - ) -} diff --git a/src/components/Payment/Views/MantecaFulfillment.view.tsx b/src/components/Payment/Views/MantecaFulfillment.view.tsx deleted file mode 100644 index 9fc6d5a95..000000000 --- a/src/components/Payment/Views/MantecaFulfillment.view.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useEffect, useState, useMemo } from 'react' -import { type CountryData } from '@/components/AddMoney/consts' -import MantecaDepositShareDetails from '@/components/AddMoney/components/MantecaDepositShareDetails' -import PeanutLoading from '@/components/Global/PeanutLoading' -import { MantecaGeoSpecificKycModal } from '@/components/Kyc/InitiateMantecaKYCModal' -import { useAuth } from '@/context/authContext' -import { useRequestFulfillmentFlow } from '@/context/RequestFulfillmentFlowContext' -import { usePaymentStore } from '@/redux/hooks' -import { mantecaApi } from '@/services/manteca' -import { useQuery } from '@tanstack/react-query' -import useKycStatus from '@/hooks/useKycStatus' -import ErrorAlert from '@/components/Global/ErrorAlert' - -const MantecaFulfillment = () => { - const { setFulfillUsingManteca, selectedCountry, setSelectedCountry } = useRequestFulfillmentFlow() - const { requestDetails, chargeDetails } = usePaymentStore() - const [isKYCModalOpen, setIsKYCModalOpen] = useState(false) - const { isUserMantecaKycApproved, isUserBridgeKycApproved } = useKycStatus() - const { fetchUser } = useAuth() - - const currency = selectedCountry?.currency || 'ARS' - const { - data: depositData, - isLoading: isLoadingDeposit, - isError, - error, - } = useQuery({ - queryKey: ['manteca-deposit', chargeDetails?.uuid, currency], - queryFn: () => - mantecaApi.deposit({ - usdAmount: requestDetails?.tokenAmount || chargeDetails?.tokenAmount || '0', - currency, - chargeId: chargeDetails?.uuid, - }), - refetchOnWindowFocus: false, - staleTime: Infinity, // don't refetch the data - enabled: Boolean(chargeDetails?.uuid) && isUserMantecaKycApproved, - }) - - const errorMessage = useMemo(() => { - if (error) { - return error.message - } - if (!chargeDetails) { - return 'Charge details not found' - } - }, [error, chargeDetails]) - - const argentinaCountryData = { - id: 'AR', - type: 'country', - title: 'Argentina', - currency: 'ARS', - path: 'argentina', - iso2: 'AR', - iso3: 'ARG', - } as CountryData - - const handleKycCancel = () => { - setIsKYCModalOpen(false) - setSelectedCountry(null) - setFulfillUsingManteca(false) - } - - const handleBackClick = () => { - // reset manteca fulfillment state to show payment options again - setFulfillUsingManteca(false) - setSelectedCountry(null) - } - - useEffect(() => { - if (!isUserMantecaKycApproved) { - setIsKYCModalOpen(true) - } - }, [isUserMantecaKycApproved]) - - if (isLoadingDeposit) { - return - } - - return ( -
- {depositData?.data && ( - - )} - {errorMessage && } - - {isKYCModalOpen && ( - { - // close the modal and let the user continue with amount input - setIsKYCModalOpen(false) - fetchUser() - }} - selectedCountry={selectedCountry || argentinaCountryData} - /> - )} -
- ) -} - -export default MantecaFulfillment diff --git a/src/components/Profile/AvatarWithBadge.tsx b/src/components/Profile/AvatarWithBadge.tsx index 0a326da32..486562f93 100644 --- a/src/components/Profile/AvatarWithBadge.tsx +++ b/src/components/Profile/AvatarWithBadge.tsx @@ -1,9 +1,8 @@ -import { getInitialsFromName } from '@/utils' +import { getInitialsFromName } from '@/utils/general.utils' import { getColorForUsername } from '@/utils/color.utils' import React, { useMemo } from 'react' import { twMerge } from 'tailwind-merge' import { Icon, type IconName } from '../Global/Icons/Icon' -import StatusPill, { type StatusPillType } from '../Global/StatusPill' import Image, { type StaticImageData } from 'next/image' export type AvatarSize = 'tiny' | 'extra-small' | 'small' | 'medium' | 'large' diff --git a/src/components/Profile/components/IdentityVerificationCountryList.tsx b/src/components/Profile/components/IdentityVerificationCountryList.tsx index b38f5243f..4146a8dad 100644 --- a/src/components/Profile/components/IdentityVerificationCountryList.tsx +++ b/src/components/Profile/components/IdentityVerificationCountryList.tsx @@ -4,23 +4,30 @@ import { SearchInput } from '@/components/SearchInput' import { getCountriesForRegion } from '@/utils/identityVerification' import { MantecaSupportedExchanges } from '@/components/AddMoney/consts' import StatusBadge from '@/components/Global/Badges/StatusBadge' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import * as Accordion from '@radix-ui/react-accordion' import { useRouter } from 'next/navigation' import { useState } from 'react' import CountryListSection from './CountryListSection' +import ActionModal from '@/components/Global/ActionModal' const IdentityVerificationCountryList = ({ region }: { region: string }) => { const [searchTerm, setSearchTerm] = useState('') const router = useRouter() + const [isUnavailableModalOpen, setIsUnavailableModalOpen] = useState(false) + const [selectedUnavailableCountry, setSelectedUnavailableCountry] = useState(null) - const { supportedCountries, unsupportedCountries } = getCountriesForRegion(region) + const { supportedCountries, limitedAccessCountries, unsupportedCountries } = getCountriesForRegion(region) // Filter both arrays based on search term const filteredSupportedCountries = supportedCountries.filter((country) => country.title.toLowerCase().includes(searchTerm.toLowerCase()) ) + const filteredLimitedAccessCountries = limitedAccessCountries.filter((country) => + country.title.toLowerCase().includes(searchTerm.toLowerCase()) + ) + const filteredUnsupportedCountries = unsupportedCountries.filter((country) => country.title.toLowerCase().includes(searchTerm.toLowerCase()) ) @@ -63,7 +70,7 @@ const IdentityVerificationCountryList = ({ region }: { region: string }) => { value="limited-access" title="Limited access" description="These countries support verification, but don't have full payment support yet." - countries={filteredUnsupportedCountries} + countries={filteredLimitedAccessCountries} onCountryClick={(country) => { // Check if country is in MantecaSupportedExchanges const countryCode = country.iso2?.toUpperCase() @@ -94,7 +101,52 @@ const IdentityVerificationCountryList = ({ region }: { region: string }) => { )} defaultOpen /> + + {filteredUnsupportedCountries.length > 0 && ( + { + setSelectedUnavailableCountry(country.title) + setIsUnavailableModalOpen(true) + }} + rightContent={() => ( +
+ +
+ )} + defaultOpen + /> + )} + + { + setSelectedUnavailableCountry(null) + setIsUnavailableModalOpen(false) + }} + ctas={[ + { + text: 'I Understand', + shadowSize: '4', + onClick: () => { + setSelectedUnavailableCountry(null) + setIsUnavailableModalOpen(false) + }, + }, + ]} + />
) } diff --git a/src/components/Profile/components/ProfileHeader.tsx b/src/components/Profile/components/ProfileHeader.tsx index 334330807..8bd05c492 100644 --- a/src/components/Profile/components/ProfileHeader.tsx +++ b/src/components/Profile/components/ProfileHeader.tsx @@ -1,4 +1,4 @@ -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { BASE_URL } from '@/components/Global/DirectSendQR/utils' import { Icon } from '@/components/Global/Icons/Icon' import React, { useState } from 'react' diff --git a/src/components/Profile/components/PublicProfile.tsx b/src/components/Profile/components/PublicProfile.tsx index 7928ea90d..ef6137a33 100644 --- a/src/components/Profile/components/PublicProfile.tsx +++ b/src/components/Profile/components/PublicProfile.tsx @@ -1,19 +1,17 @@ 'use client' import { HandThumbsUpV2, PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { Icon } from '@/components/Global/Icons/Icon' import NavHeader from '@/components/Global/NavHeader' import HomeHistory from '@/components/Home/HomeHistory' -import { useAppDispatch } from '@/redux/hooks' -import { paymentActions } from '@/redux/slices/payment-slice' import Image from 'next/image' import ProfileHeader from './ProfileHeader' import { useState, useEffect, useMemo } from 'react' import { usersApi } from '@/services/users' import { useRouter } from 'next/navigation' import Card from '@/components/Global/Card' -import { checkIfInternalNavigation } from '@/utils' +import { checkIfInternalNavigation } from '@/utils/general.utils' import { useAuth } from '@/context/authContext' import ShareButton from '@/components/Global/ShareButton' import ActionModal from '@/components/Global/ActionModal' @@ -27,7 +25,6 @@ interface PublicProfileProps { } const PublicProfile: React.FC = ({ username, isLoggedIn = false, onSendClick }) => { - const dispatch = useAppDispatch() const [totalSentByLoggedInUser, setTotalSentByLoggedInUser] = useState('0') const [fullName, setFullName] = useState(username) const [showFullName, setShowFullName] = useState(false) @@ -45,12 +42,10 @@ const PublicProfile: React.FC = ({ username, isLoggedIn = fa earnedAt?: string | Date }> >([]) - // Handle send button click + // handle send button click const handleSend = () => { if (onSendClick) { onSendClick() - } else { - dispatch(paymentActions.setView('INITIAL')) } } diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index 9d5c4fc71..cbf3439b1 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -1,13 +1,13 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { Icon } from '@/components/Global/Icons/Icon' import { useAuth } from '@/context/authContext' import NavHeader from '../Global/NavHeader' import ProfileHeader from './components/ProfileHeader' import ProfileMenuItem from './components/ProfileMenuItem' import { useRouter } from 'next/navigation' -import { checkIfInternalNavigation, generateInviteCodeLink, generateInvitesShareText } from '@/utils' +import { checkIfInternalNavigation, generateInviteCodeLink, generateInvitesShareText } from '@/utils/general.utils' import ActionModal from '../Global/ActionModal' import { useState } from 'react' import useKycStatus from '@/hooks/useKycStatus' @@ -99,6 +99,13 @@ export const Profile = () => {
+ router.push('/profile/backup')} + position="last" + /> {/* Enable with Account Management project. */} {/* { const router = useRouter() diff --git a/src/components/Refund/index.tsx b/src/components/Refund/index.tsx index 02a820395..30b167326 100644 --- a/src/components/Refund/index.tsx +++ b/src/components/Refund/index.tsx @@ -3,16 +3,16 @@ import peanut from '@squirrel-labs/peanut-sdk' import { useForm } from 'react-hook-form' import { useConfig, useSendTransaction } from 'wagmi' -import * as consts from '@/constants' +import { supportedPeanutChains } from '@/constants/general.consts' import { loadingStateContext } from '@/context' import { useWallet } from '@/hooks/wallet/useWallet' -import { useAppDispatch } from '@/redux/hooks' -import { getExplorerUrl } from '@/utils' +import { getExplorerUrl } from '@/utils/general.utils' import * as Sentry from '@sentry/nextjs' import { useContext, useState } from 'react' import { waitForTransactionReceipt } from 'wagmi/actions' -import { walletActions } from '../../redux/slices/wallet-slice' -import { Button, Card } from '../0_Bruddle' +import { useModalsContext } from '@/context/ModalsContext' +import { Button } from '@/components/0_Bruddle/Button' +import { Card } from '@/components/0_Bruddle/Card' import BaseInput from '../0_Bruddle/BaseInput' import PageContainer from '../0_Bruddle/PageContainer' import Select from '../Global/Select' @@ -21,7 +21,7 @@ export const Refund = () => { const { isConnected } = useWallet() const { sendTransactionAsync } = useSendTransaction() const config = useConfig() - const dispatch = useAppDispatch() + const { setIsSignInModalOpen } = useModalsContext() const [errorState, setErrorState] = useState<{ showError: boolean @@ -140,12 +140,12 @@ export const Refund = () => { classButton="h-auto px-0 border-none bg-trasparent text-sm !font-normal" classOptions="-left-4 -right-3 w-auto py-1 overflow-auto max-h-36" classArrow="ml-1" - items={consts.supportedPeanutChains.map((chain) => ({ + items={supportedPeanutChains.map((chain) => ({ id: chain.chainId, title: chain.name, }))} value={ - consts.supportedPeanutChains + supportedPeanutChains .map((c) => ({ id: c.chainId, title: c.name })) .find((i) => i.id === refundFormWatch.chainId) ?? null } @@ -172,7 +172,7 @@ export const Refund = () => { type={isConnected ? 'submit' : 'button'} onClick={() => { if (!isConnected) { - dispatch(walletActions.setSignInModalVisible(true)) + setIsSignInModalOpen(true) } }} disabled={isLoading || claimedExploredUrlWithHash ? true : false} diff --git a/src/components/Request/Pay/Pay.tsx b/src/components/Request/Pay/Pay.tsx index 5e69b08ff..3b7ecfd6c 100644 --- a/src/components/Request/Pay/Pay.tsx +++ b/src/components/Request/Pay/Pay.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react' import { useSearchParams } from 'next/navigation' -import { getRequestLink } from '@/utils' +import { getRequestLink } from '@/utils/general.utils' import { useRouter } from 'next/navigation' import { chargesApi } from '@/services/charges' @@ -19,7 +19,6 @@ export const PayRequestLink = () => { chainId: charge.chainId, tokenAmount: charge.tokenAmount, tokenSymbol: charge.tokenSymbol, - chargeId: charge.uuid, }) router.push(link) return diff --git a/src/components/Request/direct-request/views/Initial.direct.request.view.tsx b/src/components/Request/direct-request/views/Initial.direct.request.view.tsx index 44ee96199..c25bc440d 100644 --- a/src/components/Request/direct-request/views/Initial.direct.request.view.tsx +++ b/src/components/Request/direct-request/views/Initial.direct.request.view.tsx @@ -1,20 +1,21 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import ErrorAlert from '@/components/Global/ErrorAlert' import FileUploadInput from '@/components/Global/FileUploadInput' import GeneralRecipientInput, { type GeneralRecipientUpdate } from '@/components/Global/GeneralRecipientInput' import NavHeader from '@/components/Global/NavHeader' import PeanutLoading from '@/components/Global/PeanutLoading' -import TokenAmountInput from '@/components/Global/TokenAmountInput' +import AmountInput from '@/components/Global/AmountInput' import ValidationErrorView, { type ValidationErrorViewProps } from '@/components/Payment/Views/Error.validation.view' -import DirectSuccessView from '@/components/Payment/Views/Status.payment.view' +import PaymentSuccessView from '@/features/payments/shared/components/PaymentSuccessView' import UserCard from '@/components/User/UserCard' import { loadingStateContext } from '@/context' import { useWallet } from '@/hooks/wallet/useWallet' import { useUserStore } from '@/redux/hooks' -import { type IAttachmentOptions } from '@/redux/types/send-flow.types' +import { type IAttachmentOptions } from '@/interfaces/attachment' import { usersApi } from '@/services/users' -import { formatAmount, printableUsdc } from '@/utils' +import { formatAmount } from '@/utils/general.utils' +import { printableUsdc } from '@/utils/balance.utils' import { captureException } from '@sentry/nextjs' import { useRouter } from 'next/navigation' import { useCallback, useContext, useEffect, useMemo, useState } from 'react' @@ -189,7 +190,7 @@ const DirectRequestInitialView = ({ username }: DirectRequestInitialViewProps) = )}
-
- setView('confirm')} walletBalance={peanutWalletBalance} hideCurrencyToggle diff --git a/src/components/Request/link/views/Create.request.link.view.tsx b/src/components/Request/link/views/Create.request.link.view.tsx index 80a64e634..13fc3f5e7 100644 --- a/src/components/Request/link/views/Create.request.link.view.tsx +++ b/src/components/Request/link/views/Create.request.link.view.tsx @@ -1,6 +1,6 @@ 'use client' import { fetchTokenDetails } from '@/app/actions/tokens' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { useToast } from '@/components/0_Bruddle/Toast' import FileUploadInput from '@/components/Global/FileUploadInput' import Loading from '@/components/Global/Loading' @@ -8,22 +8,24 @@ import NavHeader from '@/components/Global/NavHeader' import PeanutActionCard from '@/components/Global/PeanutActionCard' import QRCodeWrapper from '@/components/Global/QRCodeWrapper' import ShareButton from '@/components/Global/ShareButton' -import TokenAmountInput from '@/components/Global/TokenAmountInput' -import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants' +import AmountInput from '@/components/Global/AmountInput' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants/zerodev.consts' import { TRANSACTIONS } from '@/constants/query.consts' import * as context from '@/context' import { useAuth } from '@/context/authContext' import { useDebounce } from '@/hooks/useDebounce' import { useWallet } from '@/hooks/wallet/useWallet' import { type IToken } from '@/interfaces' -import { type IAttachmentOptions } from '@/redux/types/send-flow.types' +import { type IAttachmentOptions } from '@/interfaces/attachment' import { requestsApi } from '@/services/requests' -import { fetchTokenSymbol, formatTokenAmount, getRequestLink, isNativeCurrency, printableUsdc } from '@/utils' +import { fetchTokenSymbol, formatTokenAmount, getRequestLink, isNativeCurrency } from '@/utils/general.utils' +import { printableUsdc } from '@/utils/balance.utils' import * as Sentry from '@sentry/nextjs' import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' import { useQueryClient } from '@tanstack/react-query' import { useRouter, useSearchParams } from 'next/navigation' import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' +import { Icon as IconComponent } from '@/components/Global/Icons/Icon' export const CreateRequestLinkView = () => { const toast = useToast() @@ -354,15 +356,22 @@ export const CreateRequestLinkView = () => { isLoading={isCreatingLink || isUpdatingRequest} /> - + +

+ {' '} + Leave empty to let payers choose amounts. +

+
+ } /> { - // props and basic setup - const { user, fetchUser } = useAuth() - const { createOnramp } = useCreateOnramp() - const { chargeDetails } = usePaymentStore() - const { requestType } = useDetermineBankRequestType(chargeDetails?.requestLink.recipientAccount.userId ?? '') - const [isUpdatingUser, setIsUpdatingUser] = useState(false) - const [errorMessage, setErrorMessage] = useState(null) - const [isUserDetailsFormValid, setIsUserDetailsFormValid] = useState(false) - const formRef = useRef<{ handleSubmit: () => void }>(null) - const [isKycModalOpen, setIsKycModalOpen] = useState(false) - - // state from the centralized context - const { - flowStep: requestFulfilmentBankFlowStep, - setFlowStep: setRequestFulfilmentBankFlowStep, - selectedCountry, - setOnrampData, - requesterDetails, - showVerificationModal, - setShowVerificationModal, - } = useRequestFulfillmentFlow() - - useEffect(() => { - if (showVerificationModal) { - setIsKycModalOpen(true) - } - }, [showVerificationModal]) - - useEffect(() => { - if (!chargeDetails || !selectedCountry) return - const { currency } = getCurrencyConfig(selectedCountry.id, 'onramp') - const usdAmount = chargeDetails.tokenAmount - const minAmount = getMinimumAmount(selectedCountry.id) - getCurrencyPrice(currency).then((price) => { - const currencyAmount = Number(usdAmount) * price.buy - if (currencyAmount < minAmount) { - setErrorMessage(`Minimum amount is ${minAmount.toFixed(2)} ${currency}`) - } else { - setErrorMessage(null) - } - }) - }, [chargeDetails, selectedCountry]) - - const handleOnrampConfirmation = async () => { - if (!selectedCountry) return - setErrorMessage(null) - try { - let onrampDataResponse - - if (requestType === BankRequestType.GuestBankRequest) { - onrampDataResponse = await createOnrampForGuest({ - amount: chargeDetails?.tokenAmount ?? '0', - country: selectedCountry, - userId: requesterDetails?.userId ?? '', - chargeId: chargeDetails?.uuid, - }) - } else { - onrampDataResponse = await createOnramp({ - usdAmount: chargeDetails?.tokenAmount ?? '0', - country: selectedCountry, - chargeId: chargeDetails?.uuid, - recipientAddress: parsedPaymentData.recipient.resolvedAddress as Address, - }) - } - - if ( - requestType === BankRequestType.GuestBankRequest && - onrampDataResponse.data && - onrampDataResponse.data.transferId - ) { - setOnrampData(onrampDataResponse.data) - setRequestFulfilmentBankFlowStep(RequestFulfillmentBankFlowStep.DepositBankDetails) - } else if (onrampDataResponse.transferId) { - setOnrampData(onrampDataResponse) - setRequestFulfilmentBankFlowStep(RequestFulfillmentBankFlowStep.DepositBankDetails) - } else { - console.error('Onramp creation response issue:', onrampDataResponse) - setRequestFulfilmentBankFlowStep(RequestFulfillmentBankFlowStep.BankCountryList) - } - } catch (error) { - console.error('Failed to create onramp', error) - setRequestFulfilmentBankFlowStep(RequestFulfillmentBankFlowStep.BankCountryList) - } - } - - const handleKycSuccess = () => { - setShowVerificationModal(false) - setRequestFulfilmentBankFlowStep(RequestFulfillmentBankFlowStep.BankCountryList) - fetchUser() - } - - const handleUserDetailsSubmit = async (data: UserDetailsFormData) => { - setIsUpdatingUser(true) - setErrorMessage(null) - try { - if (!user?.user.userId) throw new Error('User not found') - const result = await updateUserById({ - userId: user.user.userId, - fullName: `${data.fullName}`, - email: data.email, - }) - if (result.error) { - throw new Error(result.error) - } - await fetchUser() - setShowVerificationModal(true) - } catch (error: any) { - setErrorMessage(error.message) - return { error: error.message } - } finally { - setIsUpdatingUser(false) - } - return {} - } - - const [firstName, ...lastNameParts] = (user?.user.fullName ?? '').split(' ') - const lastName = lastNameParts.join(' ') - - const initialUserDetails: Partial = useMemo( - () => ({ - firstName: user?.user.fullName ? firstName : '', - lastName: user?.user.fullName ? lastName : '', - email: user?.user.email ?? '', - }), - [user?.user.fullName, user?.user.email, firstName, lastName] - ) - - // main render logic based on the current flow step - if (showVerificationModal) { - return ( - { - setIsKycModalOpen(false) - }} - onKycSuccess={handleKycSuccess} - onManualClose={() => { - setShowVerificationModal(false) - setRequestFulfilmentBankFlowStep(RequestFulfillmentBankFlowStep.BankCountryList) - }} - flow="request_fulfillment" - /> - ) - } - - switch (requestFulfilmentBankFlowStep) { - case RequestFulfillmentBankFlowStep.BankCountryList: - return ( - - ) - case RequestFulfillmentBankFlowStep.OnrampConfirmation: - return ( - { - setRequestFulfilmentBankFlowStep(RequestFulfillmentBankFlowStep.BankCountryList) - }} - onConfirm={() => { - handleOnrampConfirmation() - }} - amount={chargeDetails?.tokenAmount ?? '0'} - currency={'$'} - /> - ) - case RequestFulfillmentBankFlowStep.DepositBankDetails: - return - - case RequestFulfillmentBankFlowStep.CollectUserDetails: - return ( -
- setRequestFulfilmentBankFlowStep(RequestFulfillmentBankFlowStep.BankCountryList)} - title="Identity Verification" - /> -
-

Verify your details

- - - {errorMessage && } -
-
- ) - default: - return null - } -} diff --git a/src/components/SearchInput/index.tsx b/src/components/SearchInput/index.tsx index e00cae23f..7a482d643 100644 --- a/src/components/SearchInput/index.tsx +++ b/src/components/SearchInput/index.tsx @@ -1,5 +1,5 @@ import { Icon } from '@/components/Global/Icons/Icon' -import { Button } from '../0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import BaseInput from '../0_Bruddle/BaseInput' interface SearchInputProps { diff --git a/src/components/Send/link/LinkSendFlowManager.tsx b/src/components/Send/link/LinkSendFlowManager.tsx index d12ea45a9..f80923780 100644 --- a/src/components/Send/link/LinkSendFlowManager.tsx +++ b/src/components/Send/link/LinkSendFlowManager.tsx @@ -1,22 +1,22 @@ 'use client' -import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN, SQUID_API_URL, SQUID_INTEGRATOR_ID } from '@/constants' import { tokenSelectorContext } from '@/context' -import { useAppDispatch, useSendFlowStore } from '@/redux/hooks' -import { sendFlowActions } from '@/redux/slices/send-flow-slice' -import { fetchWithSentry } from '@/utils' +import { LinkSendFlowProvider, useLinkSendFlow } from '@/context/LinkSendFlowContext' +import { fetchWithSentry } from '@/utils/sentry.utils' import { useContext, useEffect } from 'react' import NavHeader from '../../Global/NavHeader' import LinkSendInitialView from './views/Initial.link.send.view' import LinkSendSuccessView from './views/Success.link.send.view' +import { SQUID_INTEGRATOR_ID, SQUID_API_URL } from '@/constants/general.consts' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants/zerodev.consts' interface LinkSendFlowManagerProps { onPrev?: () => void } -const LinkSendFlowManager = ({ onPrev }: LinkSendFlowManagerProps) => { - const dispatch = useAppDispatch() - const { view } = useSendFlowStore() +// inner component that uses the context +const LinkSendFlowContent = ({ onPrev }: LinkSendFlowManagerProps) => { + const { view, setCrossChainDetails } = useLinkSendFlow() const { resetTokenContextProvider, setSelectedChainID, setSelectedTokenAddress } = useContext(tokenSelectorContext) const fetchAndSetCrossChainDetails = async () => { @@ -30,8 +30,7 @@ const LinkSendFlowManager = ({ onPrev }: LinkSendFlowManagerProps) => { throw new Error('Squid: Network response was not ok') } const data = await response.json() - - dispatch(sendFlowActions.setCrossChainDetails(data.chains)) + setCrossChainDetails(data.chains) } useEffect(() => { @@ -61,4 +60,13 @@ const LinkSendFlowManager = ({ onPrev }: LinkSendFlowManagerProps) => { ) } +// wrapper component that provides the context +const LinkSendFlowManager = ({ onPrev }: LinkSendFlowManagerProps) => { + return ( + + + + ) +} + export default LinkSendFlowManager diff --git a/src/components/Send/link/views/Initial.link.send.view.tsx b/src/components/Send/link/views/Initial.link.send.view.tsx index 007b69988..a742bc933 100644 --- a/src/components/Send/link/views/Initial.link.send.view.tsx +++ b/src/components/Send/link/views/Initial.link.send.view.tsx @@ -3,30 +3,38 @@ import { useCreateLink } from '@/components/Create/useCreateLink' import ErrorAlert from '@/components/Global/ErrorAlert' import PeanutActionCard from '@/components/Global/PeanutActionCard' -import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants' +import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' import { TRANSACTIONS } from '@/constants/query.consts' import { loadingStateContext } from '@/context' +import { useLinkSendFlow } from '@/context/LinkSendFlowContext' import { useWallet } from '@/hooks/wallet/useWallet' -import { useAppDispatch, useSendFlowStore } from '@/redux/hooks' -import { sendFlowActions } from '@/redux/slices/send-flow-slice' import { sendLinksApi } from '@/services/sendLinks' -import { ErrorHandler, printableUsdc } from '@/utils' +import { ErrorHandler } from '@/utils/sdkErrorHandler.utils' +import { printableUsdc } from '@/utils/balance.utils' import { captureException } from '@sentry/nextjs' import { useQueryClient } from '@tanstack/react-query' import { useCallback, useContext, useEffect, useMemo } from 'react' import { parseUnits } from 'viem' -import { Button } from '../../../0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import FileUploadInput from '../../../Global/FileUploadInput' -import TokenAmountInput from '../../../Global/TokenAmountInput' +import AmountInput from '../../../Global/AmountInput' import { usePendingTransactions } from '@/hooks/wallet/usePendingTransactions' const LinkSendInitialView = () => { - const dispatch = useAppDispatch() - const { attachmentOptions, errorState, tokenValue } = useSendFlowStore() + const { + attachmentOptions, + setAttachmentOptions, + errorState, + setErrorState, + tokenValue, + setTokenValue, + setLink, + setView, + } = useLinkSendFlow() const { createLink } = useCreateLink() - const { setLoadingState, loadingState, isLoading } = useContext(loadingStateContext) + const { setLoadingState, isLoading } = useContext(loadingStateContext) const { fetchBalance, balance } = useWallet() const queryClient = useQueryClient() @@ -43,24 +51,19 @@ const LinkSendInitialView = () => { setLoadingState('Loading') // clear any previous errors - dispatch( - sendFlowActions.setErrorState({ - showError: false, - errorMessage: '', - }) - ) + setErrorState({ showError: false, errorMessage: '' }) const { link, pubKey, chainId, contractVersion, depositIdx, txHash, amount, tokenAddress } = await createLink(parseUnits(tokenValue!, PEANUT_WALLET_TOKEN_DECIMALS)) - dispatch(sendFlowActions.setLink(link)) - dispatch(sendFlowActions.setView('SUCCESS')) + setLink(link) + setView('SUCCESS') fetchBalance() queryClient.invalidateQueries({ queryKey: [TRANSACTIONS], }) - // We dont need to wait for this to finish in order to proceed + // we dont need to wait for this to finish in order to proceed setTimeout(async () => { try { await sendLinksApi.create({ @@ -77,7 +80,7 @@ const LinkSendInitialView = () => { mimetype: attachmentOptions?.rawFile?.type, }) } catch (error) { - // We want to capture any errors here because we are already in the background + // we want to capture any errors here because we are already in the background console.error(error) captureException(error) } @@ -85,20 +88,26 @@ const LinkSendInitialView = () => { } catch (error) { // handle errors const errorString = ErrorHandler(error) - dispatch( - sendFlowActions.setErrorState({ - showError: true, - errorMessage: errorString, - }) - ) + setErrorState({ showError: true, errorMessage: errorString }) captureException(error) } finally { setLoadingState('Idle') } - }, [isLoading, tokenValue, createLink, fetchBalance, dispatch, queryClient, setLoadingState, attachmentOptions]) + }, [ + isLoading, + tokenValue, + createLink, + fetchBalance, + queryClient, + setLoadingState, + attachmentOptions, + setLink, + setView, + setErrorState, + ]) useEffect(() => { - // Skip balance check if transaction is pending + // skip balance check if transaction is pending // (balance may be optimistically updated during transaction) // isLoading covers the createLink operation which directly uses handleSendUserOpEncoded if (hasPendingTransactions || isLoading) { @@ -106,43 +115,27 @@ const LinkSendInitialView = () => { } if (!peanutWalletBalance || !tokenValue) { - // Clear error state when no balance or token value - dispatch( - sendFlowActions.setErrorState({ - showError: false, - errorMessage: '', - }) - ) + // clear error state when no balance or token value + setErrorState({ showError: false, errorMessage: '' }) return } if ( parseUnits(peanutWalletBalance, PEANUT_WALLET_TOKEN_DECIMALS) < parseUnits(tokenValue, PEANUT_WALLET_TOKEN_DECIMALS) ) { - dispatch( - sendFlowActions.setErrorState({ - showError: true, - errorMessage: 'Insufficient balance', - }) - ) + setErrorState({ showError: true, errorMessage: 'Insufficient balance' }) } else { - dispatch( - sendFlowActions.setErrorState({ - showError: false, - errorMessage: '', - }) - ) + setErrorState({ showError: false, errorMessage: '' }) } - }, [peanutWalletBalance, tokenValue, dispatch, hasPendingTransactions, isLoading]) + }, [peanutWalletBalance, tokenValue, setErrorState, hasPendingTransactions, isLoading]) return (
- dispatch(sendFlowActions.setTokenValue(value))} + @@ -151,7 +144,7 @@ const LinkSendInitialView = () => { className="h-11" placeholder="Comment" attachmentOptions={attachmentOptions} - setAttachmentOptions={(options) => dispatch(sendFlowActions.setAttachmentOptions(options))} + setAttachmentOptions={setAttachmentOptions} />
diff --git a/src/components/Send/link/views/Success.link.send.view.tsx b/src/components/Send/link/views/Success.link.send.view.tsx index 2c0a70004..e4003d405 100644 --- a/src/components/Send/link/views/Success.link.send.view.tsx +++ b/src/components/Send/link/views/Success.link.send.view.tsx @@ -4,14 +4,12 @@ import { Button } from '@/components/0_Bruddle/Button' import CancelSendLinkModal from '@/components/Global/CancelSendLinkModal' import { Icon } from '@/components/Global/Icons/Icon' import NavHeader from '@/components/Global/NavHeader' -import PeanutLoading from '@/components/Global/PeanutLoading' import QRCodeWrapper from '@/components/Global/QRCodeWrapper' import ShareButton from '@/components/Global/ShareButton' import { SuccessViewDetailsCard } from '@/components/Global/SuccessViewComponents/SuccessViewDetailsCard' import { useWallet } from '@/hooks/wallet/useWallet' -import { useAppDispatch, useSendFlowStore, useUserStore } from '@/redux/hooks' -import { sendFlowActions } from '@/redux/slices/send-flow-slice' -import { sendLinksApi } from '@/services/sendLinks' +import { useLinkSendFlow } from '@/context/LinkSendFlowContext' +import { useUserStore } from '@/redux/hooks' import { captureException } from '@sentry/nextjs' import { useQueryClient } from '@tanstack/react-query' import { useRouter } from 'next/navigation' @@ -21,9 +19,8 @@ import { useToast } from '@/components/0_Bruddle/Toast' import { TRANSACTIONS } from '@/constants/query.consts' const LinkSendSuccessView = () => { - const dispatch = useAppDispatch() const router = useRouter() - const { link, attachmentOptions, tokenValue } = useSendFlowStore() + const { link, attachmentOptions, tokenValue, resetLinkSendFlow } = useLinkSendFlow() const queryClient = useQueryClient() const { fetchBalance } = useWallet() const { user } = useUserStore() @@ -37,9 +34,9 @@ const LinkSendSuccessView = () => { useEffect(() => { return () => { // clear state on unmount - dispatch(sendFlowActions.resetSendFlow()) + resetLinkSendFlow() } - }, [dispatch]) + }, [resetLinkSendFlow]) return (
@@ -48,7 +45,7 @@ const LinkSendSuccessView = () => { title="Send" onPrev={() => { router.push('/home') - dispatch(sendFlowActions.resetSendFlow()) + resetLinkSendFlow() }} />
diff --git a/src/components/Send/views/Contacts.view.tsx b/src/components/Send/views/Contacts.view.tsx index 0fd722b1f..148b4e59b 100644 --- a/src/components/Send/views/Contacts.view.tsx +++ b/src/components/Send/views/Contacts.view.tsx @@ -1,26 +1,32 @@ 'use client' -import { useAppDispatch } from '@/redux/hooks' -import { sendFlowActions } from '@/redux/slices/send-flow-slice' import { useRouter, useSearchParams } from 'next/navigation' import NavHeader from '@/components/Global/NavHeader' import { ActionListCard } from '@/components/ActionListCard' import { useContacts } from '@/hooks/useContacts' import { useInfiniteScroll } from '@/hooks/useInfiniteScroll' -import { useMemo, useState } from 'react' +import { useState, useEffect } from 'react' import AvatarWithBadge from '@/components/Profile/AvatarWithBadge' import { VerifiedUserLabel } from '@/components/UserHeader' import { SearchInput } from '@/components/SearchInput' import PeanutLoading from '@/components/Global/PeanutLoading' import EmptyState from '@/components/Global/EmptyStates/EmptyState' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' +import { useDebounce } from '@/hooks/useDebounce' +import { ContactsListSkeleton } from '@/components/Common/ContactsListSkeleton' export default function ContactsView() { const router = useRouter() - const dispatch = useAppDispatch() const searchParams = useSearchParams() const isSendingByLink = searchParams.get('view') === 'link' || searchParams.get('createLink') === 'true' const isSendingToContacts = searchParams.get('view') === 'contacts' + const [searchQuery, setSearchQuery] = useState('') + const [hasLoadedOnce, setHasLoadedOnce] = useState(false) + + // debounce search query to avoid excessive API calls + const debouncedSearchQuery = useDebounce(searchQuery, 300) + + // fetch contacts with server-side search const { contacts, isLoading: isFetchingContacts, @@ -29,39 +35,33 @@ export default function ContactsView() { hasNextPage, isFetchingNextPage, refetch, - } = useContacts({ limit: 50 }) - const [searchQuery, setSearchQuery] = useState('') + } = useContacts({ + limit: 50, + search: debouncedSearchQuery || undefined, + }) - // infinite scroll hook - disabled when searching (search is client-side) + // infinite scroll hook - always enabled for server-side pagination const { loaderRef } = useInfiniteScroll({ hasNextPage, isFetchingNextPage, fetchNextPage, - enabled: !searchQuery, // disable when user is searching + enabled: true, }) - // client-side search filtering - const filteredContacts = useMemo(() => { - if (!searchQuery.trim()) return contacts - - const query = searchQuery.trim().toLowerCase() - return contacts.filter((contact) => { - const fullName = contact.fullName?.toLowerCase() ?? '' - return contact.username.toLowerCase().includes(query) || fullName.includes(query) - }) - }, [contacts, searchQuery]) + // track when we've loaded data at least once + useEffect(() => { + if (!hasLoadedOnce && !isFetchingContacts) { + setHasLoadedOnce(true) + } + }, [isFetchingContacts, hasLoadedOnce]) const redirectToSendByLink = () => { - // reset send flow state when entering link creation flow - dispatch(sendFlowActions.resetSendFlow()) router.push(`${window.location.pathname}?view=link`) } const handlePrev = () => { - // reset send flow state and navigate deterministically // when in sub-views (link or contacts), go back to base send page // otherwise, go to home - dispatch(sendFlowActions.resetSendFlow()) if (isSendingByLink || isSendingToContacts) { router.push('/send') } else { @@ -78,7 +78,8 @@ export default function ContactsView() { router.push(`/send/${username}`) } - if (isFetchingContacts) { + // only show full loading on initial load (before any data has been fetched) + if (isFetchingContacts && !hasLoadedOnce) { return } @@ -109,13 +110,18 @@ export default function ContactsView() { ) } + // determine if we have any contacts (initial load without search) + const hasContacts = contacts.length > 0 || !!debouncedSearchQuery + const isSearching = !!debouncedSearchQuery + const hasNoSearchResults = isSearching && contacts.length === 0 + return (
- {contacts.length > 0 ? ( + {hasContacts ? (
- {/* search input */} + {/* search input - always show when there are contacts or when searching */} setSearchQuery(e.target.value)} @@ -123,12 +129,15 @@ export default function ContactsView() { placeholder="Search contacts..." /> - {/* contacts list */} - {filteredContacts.length > 0 ? ( + {/* contacts list or search results */} + {isFetchingContacts ? ( + // show skeleton when searching/refetching + + ) : contacts.length > 0 ? (

Your contacts

- {filteredContacts.map((contact, index) => { + {contacts.map((contact, index) => { const isVerified = contact.bridgeKycStatus === 'approved' const displayName = contact.showFullName ? contact.fullName || contact.username @@ -136,11 +145,11 @@ export default function ContactsView() { return ( - {/* infinite scroll loader - only active when not searching */} - {!searchQuery && ( -
- {isFetchingNextPage && ( -
Loading more...
- )} -
- )} + {/* infinite scroll loader */} +
+ {isFetchingNextPage && ( +
Loading more...
+ )} +
- ) : ( - // no search results + ) : hasNoSearchResults ? ( + // no search results - keep search input visible - )} + ) : null}
) : ( - // empty state - no contacts at all + // empty state - no contacts at all (initial load with no contacts)
{ const router = useRouter() - const dispatch = useAppDispatch() const searchParams = useSearchParams() const isSendingByLink = searchParams.get('view') === 'link' || searchParams.get('createLink') === 'true' const isSendingToContacts = searchParams.get('view') === 'contacts' @@ -77,16 +74,12 @@ export const SendRouterView = () => { }, [isFetchingContacts, recentContactsAvatarInitials]) const redirectToSendByLink = () => { - // reset send flow state when entering link creation flow - dispatch(sendFlowActions.resetSendFlow()) router.push(`${window.location.pathname}?view=link`) } const handlePrev = () => { - // reset send flow state and navigate deterministically // when in sub-views (link or contacts), go back to base send page // otherwise, go to home - dispatch(sendFlowActions.resetSendFlow()) if (isSendingByLink || isSendingToContacts) { router.push('/send') } else { @@ -137,7 +130,7 @@ export const SendRouterView = () => { ...method, identifierIcon: (
- +
), } diff --git a/src/components/Setup/Views/InstallPWA.tsx b/src/components/Setup/Views/InstallPWA.tsx index c82699e4d..34243f177 100644 --- a/src/components/Setup/Views/InstallPWA.tsx +++ b/src/components/Setup/Views/InstallPWA.tsx @@ -1,4 +1,4 @@ -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { useToast } from '@/components/0_Bruddle/Toast' import ErrorAlert from '@/components/Global/ErrorAlert' import { Icon } from '@/components/Global/Icons/Icon' @@ -11,26 +11,22 @@ import { useRouter } from 'next/navigation' import { useCallback, useEffect, useState } from 'react' import { captureException } from '@sentry/nextjs' import { DeviceType } from '@/hooks/useGetDeviceType' +import { useBravePWAInstallState } from '@/hooks/useBravePWAInstallState' const StepTitle = ({ text }: { text: string }) =>

{text}

-// detect if the browser is brave -const isBraveBrowser = () => { - if (typeof window === 'undefined') return false - // brave browser has a specific navigator.brave property - return !!(navigator as Navigator & { brave?: { isBrave: () => Promise } }).brave -} - const InstallPWA = ({ canInstall, deferredPrompt, deviceType, screenId, + setShowBraveSuccessMessage, }: { canInstall?: boolean deferredPrompt?: BeforeInstallPromptEvent | null deviceType?: DeviceType screenId?: ScreenId + setShowBraveSuccessMessage?: (show: boolean) => void }) => { const toast = useToast() const { handleNext, isLoading: isSetupFlowLoading } = useSetupFlow() @@ -39,7 +35,8 @@ const InstallPWA = ({ const [installCancelled, setInstallCancelled] = useState(false) const [isInstallInProgress, setIsInstallInProgress] = useState(false) const [isPWAInstalled, setIsPWAInstalled] = useState(false) - const [isBrave, setIsBrave] = useState(false) + const { isBrave } = useBravePWAInstallState() + const { user } = useAuth() const { push } = useRouter() @@ -74,12 +71,7 @@ const InstallPWA = ({ useEffect(() => { if (!!user) push('/home') - }, [user]) - - // detect brave browser on mount - useEffect(() => { - setIsBrave(isBraveBrowser()) - }, []) + }, [user, push]) useEffect(() => { const handleAppInstalled = () => { @@ -100,6 +92,23 @@ const InstallPWA = ({ } }, []) + // notify parent when installation is complete on brave + useEffect(() => { + if ( + isBrave && + (isPWAInstalled || installComplete) && + !window.matchMedia('(display-mode: standalone)').matches + ) { + setShowBraveSuccessMessage?.(true) + } else { + setShowBraveSuccessMessage?.(false) + } + + return () => { + setShowBraveSuccessMessage?.(false) + } + }, [isBrave, isPWAInstalled, installComplete, setShowBraveSuccessMessage]) + useEffect(() => { if (screenId === 'pwa-install' && (deviceType === DeviceType.WEB || deviceType === DeviceType.IOS)) { const timer = setTimeout(() => { @@ -150,17 +159,7 @@ const InstallPWA = ({ // if on brave browser, show instructions instead of trying to auto-open // because brave doesn't support auto-opening pwa from browser if (isBrave) { - return ( -
-

App installed successfully!

-

- Please open the Peanut app from your home screen to continue setup. -

- {/* */} -
- ) + return null } // for other browsers, try to open the pwa in a new tab diff --git a/src/components/Setup/Views/JoinBeta.tsx b/src/components/Setup/Views/JoinBeta.tsx index 52e8e7804..b68a1f625 100644 --- a/src/components/Setup/Views/JoinBeta.tsx +++ b/src/components/Setup/Views/JoinBeta.tsx @@ -1,10 +1,10 @@ -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import BaseInput from '@/components/0_Bruddle/BaseInput' import ErrorAlert from '@/components/Global/ErrorAlert' import { useSetupFlow } from '@/hooks/useSetupFlow' import { useAppDispatch, useSetupStore } from '@/redux/hooks' import { setupActions } from '@/redux/slices/setup-slice' -import React, { useState } from 'react' +import { useState } from 'react' const JoinBeta = () => { const { handleNext } = useSetupFlow() diff --git a/src/components/Setup/Views/JoinWaitlist.tsx b/src/components/Setup/Views/JoinWaitlist.tsx index b63a35204..5c3271284 100644 --- a/src/components/Setup/Views/JoinWaitlist.tsx +++ b/src/components/Setup/Views/JoinWaitlist.tsx @@ -1,10 +1,9 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { useToast } from '@/components/0_Bruddle/Toast' import ValidatedInput from '@/components/Global/ValidatedInput' -import { useZeroDev } from '@/hooks/useZeroDev' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { twMerge } from 'tailwind-merge' import * as Sentry from '@sentry/nextjs' import { useSetupFlow } from '@/hooks/useSetupFlow' diff --git a/src/components/Setup/Views/Landing.tsx b/src/components/Setup/Views/Landing.tsx index 7f154dbb3..d14aec6e2 100644 --- a/src/components/Setup/Views/Landing.tsx +++ b/src/components/Setup/Views/Landing.tsx @@ -1,11 +1,12 @@ 'use client' -import { Button, Card } from '@/components/0_Bruddle' import { useToast } from '@/components/0_Bruddle/Toast' import { useSetupFlow } from '@/hooks/useSetupFlow' import { useLogin } from '@/hooks/useLogin' import * as Sentry from '@sentry/nextjs' import Link from 'next/link' +import { Button } from '@/components/0_Bruddle/Button' +import { Card } from '@/components/0_Bruddle/Card' const LandingStep = () => { const { handleNext } = useSetupFlow() diff --git a/src/components/Setup/Views/PasskeySetupHelpModal.tsx b/src/components/Setup/Views/PasskeySetupHelpModal.tsx index 042738966..c201081a6 100644 --- a/src/components/Setup/Views/PasskeySetupHelpModal.tsx +++ b/src/components/Setup/Views/PasskeySetupHelpModal.tsx @@ -1,9 +1,9 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import ActionModal from '@/components/Global/ActionModal' import InfoCard from '@/components/Global/InfoCard' -import { PASSKEY_TROUBLESHOOTING_STEPS, PASSKEY_WARNINGS, WebAuthnErrorName } from '@/utils' +import { PASSKEY_TROUBLESHOOTING_STEPS, PASSKEY_WARNINGS, WebAuthnErrorName } from '@/utils/webauthn.utils' interface PasskeySetupHelpModalProps { visible: boolean diff --git a/src/components/Setup/Views/SetupPasskey.tsx b/src/components/Setup/Views/SetupPasskey.tsx index d73f43374..f6994f14e 100644 --- a/src/components/Setup/Views/SetupPasskey.tsx +++ b/src/components/Setup/Views/SetupPasskey.tsx @@ -1,11 +1,13 @@ -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { useSetupStore } from '@/redux/hooks' import { useZeroDev } from '@/hooks/useZeroDev' import { useSetupFlow } from '@/hooks/useSetupFlow' import { useDeviceType } from '@/hooks/useGetDeviceType' import { useEffect, useState } from 'react' import Link from 'next/link' -import { capturePasskeyDebugInfo, checkPasskeySupport, WebAuthnErrorName, withWebAuthnRetry } from '@/utils' +import { capturePasskeyDebugInfo } from '@/utils/passkeyDebug' +import { checkPasskeySupport } from '@/utils/passkeyPreflight' +import { WebAuthnErrorName, withWebAuthnRetry } from '@/utils/webauthn.utils' import { PasskeySetupHelpModal } from './PasskeySetupHelpModal' import ErrorAlert from '@/components/Global/ErrorAlert' import * as Sentry from '@sentry/nextjs' diff --git a/src/components/Setup/Views/SignTestTransaction.tsx b/src/components/Setup/Views/SignTestTransaction.tsx index 01d1cc5e4..2b98eab6c 100644 --- a/src/components/Setup/Views/SignTestTransaction.tsx +++ b/src/components/Setup/Views/SignTestTransaction.tsx @@ -1,4 +1,4 @@ -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import { setupActions } from '@/redux/slices/setup-slice' import { useAppDispatch } from '@/redux/hooks' import { useZeroDev } from '@/hooks/useZeroDev' @@ -7,18 +7,16 @@ import { useAuth } from '@/context/authContext' import { AccountType } from '@/interfaces' import { useState, useEffect } from 'react' import { encodeFunctionData, erc20Abi, type Address, type Hex } from 'viem' -import { PEANUT_WALLET_TOKEN, PEANUT_WALLET_CHAIN } from '@/constants' -import { capturePasskeyDebugInfo } from '@/utils' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants/zerodev.consts' +import { capturePasskeyDebugInfo } from '@/utils/passkeyDebug' import * as Sentry from '@sentry/nextjs' import Link from 'next/link' -import { useRouter } from 'next/navigation' import { twMerge } from 'tailwind-merge' const SignTestTransaction = () => { - const router = useRouter() const dispatch = useAppDispatch() const { address, handleSendUserOpEncoded } = useZeroDev() - const { finalizeAccountSetup, isProcessing, error: setupError } = useAccountSetup() + const { finalizeAccountSetup, isProcessing, error: setupError, handleRedirect } = useAccountSetup() const { user, isFetchingUser, fetchUser } = useAuth() const [error, setError] = useState(null) const [isSigning, setIsSigning] = useState(false) @@ -57,10 +55,10 @@ const SignTestTransaction = () => { useEffect(() => { if (accountExists) { - console.log('[SignTestTransaction] Account exists, redirecting to home') - router.push('/home') + console.log('[SignTestTransaction] Account exists, redirecting to the app') + handleRedirect() } - }, [accountExists, router]) + }, [accountExists]) const handleTestTransaction = async () => { if (!address) { @@ -129,13 +127,12 @@ const SignTestTransaction = () => { } // account setup complete - addAccount() already fetched and verified user data - console.log('[SignTestTransaction] Account setup complete, redirecting to home') - router.push('/home') + console.log('[SignTestTransaction] Account setup complete, redirecting to the app') + // keep loading state active until redirect completes } else { // if account already exists, just navigate home (login flow) - console.log('[SignTestTransaction] Account exists, redirecting to home') - router.push('/home') + console.log('[SignTestTransaction] Account exists, redirecting to the app') // keep loading state active until redirect completes } } catch (e) { diff --git a/src/components/Setup/Views/Signup.tsx b/src/components/Setup/Views/Signup.tsx index 0efebf83d..d62fe631e 100644 --- a/src/components/Setup/Views/Signup.tsx +++ b/src/components/Setup/Views/Signup.tsx @@ -1,11 +1,11 @@ -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import ErrorAlert from '@/components/Global/ErrorAlert' import ValidatedInput from '@/components/Global/ValidatedInput' -import { next_proxy_url } from '@/constants' +import { next_proxy_url } from '@/constants/general.consts' import { useSetupFlow } from '@/hooks/useSetupFlow' import { useAppDispatch, useSetupStore } from '@/redux/hooks' import { setupActions } from '@/redux/slices/setup-slice' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import * as Sentry from '@sentry/nextjs' import Link from 'next/link' import { useState } from 'react' diff --git a/src/components/Setup/Views/Welcome.tsx b/src/components/Setup/Views/Welcome.tsx index ea375f5a6..9a69796bb 100644 --- a/src/components/Setup/Views/Welcome.tsx +++ b/src/components/Setup/Views/Welcome.tsx @@ -1,11 +1,12 @@ 'use client' -import { Button, Card } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' +import { Card } from '@/components/0_Bruddle/Card' import { useToast } from '@/components/0_Bruddle/Toast' import { useAuth } from '@/context/authContext' import { useSetupFlow } from '@/hooks/useSetupFlow' import { useZeroDev } from '@/hooks/useZeroDev' -import { getRedirectUrl, sanitizeRedirectURL, clearRedirectUrl } from '@/utils' +import { getRedirectUrl, sanitizeRedirectURL, clearRedirectUrl } from '@/utils/general.utils' import * as Sentry from '@sentry/nextjs' import { useRouter, useSearchParams } from 'next/navigation' import { useEffect } from 'react' diff --git a/src/components/Setup/components/SetupWrapper.tsx b/src/components/Setup/components/SetupWrapper.tsx index f04d32b5d..4ce9ce47c 100644 --- a/src/components/Setup/components/SetupWrapper.tsx +++ b/src/components/Setup/components/SetupWrapper.tsx @@ -1,13 +1,14 @@ import starImage from '@/assets/icons/star.png' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import CloudsBackground from '@/components/0_Bruddle/CloudsBackground' import { Icon } from '@/components/Global/Icons/Icon' import { type BeforeInstallPromptEvent, type LayoutType, type ScreenId } from '@/components/Setup/Setup.types' import InstallPWA from '@/components/Setup/Views/InstallPWA' +import { useBravePWAInstallState } from '@/hooks/useBravePWAInstallState' import { DeviceType } from '@/hooks/useGetDeviceType' import classNames from 'classnames' import Image from 'next/image' -import { Children, type ReactNode, cloneElement, memo, type ReactElement } from 'react' +import { Children, type ReactNode, cloneElement, memo, type ReactElement, useState } from 'react' import { twMerge } from 'tailwind-merge' /** @@ -208,6 +209,19 @@ export const SetupWrapper = memo( deviceType, titleClassName, }: SetupWrapperProps) => { + const { isBrave } = useBravePWAInstallState() + const [showBraveSuccessMessage, setShowBraveSuccessMessage] = useState(false) + + const shouldShowBraveInstalledHeaderOnly = + (screenId === 'pwa-install' || screenId === 'android-initial-pwa-install') && + isBrave && + showBraveSuccessMessage + + const headingTitle = shouldShowBraveInstalledHeaderOnly ? 'Success!' : title + const headingDescription = shouldShowBraveInstalledHeaderOnly + ? 'Please open the Peanut app from your home screen to continue setup.' + : description + return (
{/* navigation buttons */} @@ -248,17 +262,19 @@ export const SetupWrapper = memo( (screenId === 'signup' || screenId == 'join-beta') && 'md:max-h-12' )} > - {title && ( + {headingTitle && (

- {title} + {headingTitle}

)} - {description &&

{description}

} + {headingDescription && ( +

{headingDescription}

+ )}
{/* main content area */}
@@ -269,6 +285,7 @@ export const SetupWrapper = memo( canInstall, deviceType, screenId, + setShowBraveSuccessMessage, }) } return child diff --git a/src/components/TransactionDetails/TransactionCard.tsx b/src/components/TransactionDetails/TransactionCard.tsx index 12b3c65b4..de888e5c3 100644 --- a/src/components/TransactionDetails/TransactionCard.tsx +++ b/src/components/TransactionDetails/TransactionCard.tsx @@ -8,11 +8,10 @@ import { formatNumberForDisplay, formatCurrency, printableAddress, - getAvatarUrl, - getTransactionSign, isStableCoin, shortenStringLong, -} from '@/utils' +} from '@/utils/general.utils' +import { getAvatarUrl, getTransactionSign } from '@/utils/history.utils' import React, { lazy, Suspense } from 'react' import Image from 'next/image' import StatusPill, { type StatusPillType } from '../Global/StatusPill' @@ -58,6 +57,7 @@ interface TransactionCardProps { transaction: TransactionDetails isPending?: boolean haveSentMoneyToUser?: boolean + hideTxnAmount?: boolean } /** @@ -75,6 +75,7 @@ const TransactionCard: React.FC = ({ transaction, isPending = false, haveSentMoneyToUser = false, + hideTxnAmount = false, }) => { // hook to manage the state of the details drawer (open/closed, selected transaction) const { isDrawerOpen, selectedTransaction, openTransactionDetails, closeTransactionDetails } = @@ -195,9 +196,17 @@ const TransactionCard: React.FC = ({ ) : (
- {displayAmount} - {currencyDisplayAmount && ( - {currencyDisplayAmount} + {hideTxnAmount ? ( + **** + ) : ( + <> + {displayAmount} + {currencyDisplayAmount && ( + + {currencyDisplayAmount} + + )} + )}
diff --git a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx index f32c19cd2..0bd667c89 100644 --- a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx +++ b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx @@ -3,7 +3,7 @@ import StatusBadge, { type StatusType } from '@/components/Global/Badges/StatusBadge' import TransactionAvatarBadge from '@/components/TransactionDetails/TransactionAvatarBadge' import { type TransactionType } from '@/components/TransactionDetails/TransactionCard' -import { printableAddress } from '@/utils' +import { printableAddress } from '@/utils/general.utils' import Image from 'next/image' import React from 'react' import { isAddress as isWalletAddress } from 'viem' @@ -280,7 +280,7 @@ export const TransactionDetailsHeaderCard: React.FC diff --git a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx index 8e912bf35..dc8852918 100644 --- a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx +++ b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx @@ -16,7 +16,8 @@ import { useWallet } from '@/hooks/wallet/useWallet' import { useUserStore } from '@/redux/hooks' import { chargesApi } from '@/services/charges' import useClaimLink from '@/components/Claim/useClaimLink' -import { formatAmount, formatDate, getInitialsFromName, isStableCoin, formatCurrency, getAvatarUrl } from '@/utils' +import { formatAmount, formatDate, isStableCoin, formatCurrency } from '@/utils/general.utils' +import { getAvatarUrl } from '@/utils/history.utils' import { formatIban, getContributorsFromCharge, @@ -31,7 +32,7 @@ import { useQueryClient } from '@tanstack/react-query' import Link from 'next/link' import Image from 'next/image' import React, { useMemo, useState, useEffect } from 'react' -import { Button } from '../0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import DisplayIcon from '../Global/DisplayIcon' import { Icon } from '../Global/Icons/Icon' import { PerkIcon } from './PerkIcon' @@ -49,7 +50,7 @@ import { type TransactionDetailsRowKey, transactionDetailsRowKeys, } from './transaction-details.utils' -import { useSupportModalContext } from '@/context/SupportModalContext' +import { useModalsContext } from '@/context/ModalsContext' import { useRouter } from 'next/navigation' import { countryData } from '@/components/AddMoney/consts' import { useToast } from '@/components/0_Bruddle/Toast' @@ -60,7 +61,7 @@ import { } from '@/constants/manteca.consts' import { mantecaApi } from '@/services/manteca' import { getReceiptUrl } from '@/utils/history.utils' -import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants/zerodev.consts' import ContributorCard from '../Global/Contributors/ContributorCard' import { requestsApi } from '@/services/requests' import { PasskeyDocsLink } from '../Setup/Views/SignTestTransaction' @@ -99,7 +100,7 @@ export const TransactionDetailsReceipt = ({ const [showCancelLinkModal, setShowCancelLinkModal] = useState(false) const [tokenData, setTokenData] = useState<{ symbol: string; icon: string } | null>(null) const [isTokenDataLoading, setIsTokenDataLoading] = useState(true) - const { setIsSupportModalOpen } = useSupportModalContext() + const { setIsSupportModalOpen } = useModalsContext() const toast = useToast() const router = useRouter() const [cancelLinkText, setCancelLinkText] = useState<'Cancelling' | 'Cancelled' | 'Cancel link'>('Cancel link') @@ -160,25 +161,33 @@ export const TransactionDetailsReceipt = ({ ) ), txId: !!transaction.txHash, - cancelled: !!(transaction.status === 'cancelled' && transaction.cancelledDate), + // show cancelled row if status is cancelled, use cancelledDate or fallback to createdAt + cancelled: transaction.status === 'cancelled', claimed: !!(transaction.status === 'completed' && transaction.claimedAt), completed: !!( transaction.status === 'completed' && transaction.completedAt && transaction.extraDataForDrawer?.originalType !== EHistoryEntryType.DIRECT_SEND ), - fee: transaction.fee !== undefined, + refunded: transaction.status === 'refunded', + fee: transaction.fee !== undefined && transaction.status !== 'cancelled', exchangeRate: !!( (transaction.direction === 'bank_deposit' || transaction.direction === 'qr_payment' || transaction.direction === 'bank_withdraw') && transaction.currency?.code && - transaction.currency.code.toUpperCase() !== 'USD' + transaction.currency.code.toUpperCase() !== 'USD' && + transaction.status !== 'cancelled' + ), + bankAccountDetails: !!( + transaction.bankAccountDetails && + transaction.bankAccountDetails.identifier && + transaction.status !== 'cancelled' ), - bankAccountDetails: !!(transaction.bankAccountDetails && transaction.bankAccountDetails.identifier), transferId: !!( transaction.id && - (transaction.direction === 'bank_withdraw' || transaction.direction === 'bank_claim') + (transaction.direction === 'bank_withdraw' || transaction.direction === 'bank_claim') && + transaction.status !== 'cancelled' ), depositInstructions: !!( (transaction.extraDataForDrawer?.originalType === EHistoryEntryType.BRIDGE_ONRAMP || @@ -189,10 +198,14 @@ export const TransactionDetailsReceipt = ({ transaction.extraDataForDrawer.depositInstructions.bank_name ), peanutFee: false, // Perk fee logic removed - perks now show as separate transactions - points: !!(transaction.points && transaction.points > 0), - comment: !!transaction.memo?.trim(), - networkFee: !!(transaction.networkFeeDetails && transaction.sourceView === 'status'), - attachment: !!transaction.attachmentUrl, + points: !!(transaction.points && transaction.points > 0 && transaction.status !== 'cancelled'), + comment: !!(transaction.memo?.trim() && transaction.status !== 'cancelled'), + networkFee: !!( + transaction.networkFeeDetails && + transaction.sourceView === 'status' && + transaction.status !== 'cancelled' + ), + attachment: !!(transaction.attachmentUrl && transaction.status !== 'cancelled'), mantecaDepositInfo: !isPublic && transaction.extraDataForDrawer?.originalType === EHistoryEntryType.MANTECA_ONRAMP && @@ -621,7 +634,9 @@ export const TransactionDetailsReceipt = ({ {rowVisibilityConfig.cancelled && ( )} @@ -642,6 +657,14 @@ export const TransactionDetailsReceipt = ({ /> )} + {rowVisibilityConfig.refunded && ( + + )} + {rowVisibilityConfig.closed && ( <> {transaction.cancelledDate && ( @@ -849,12 +872,16 @@ export const TransactionDetailsReceipt = ({ value={
- {transaction.extraDataForDrawer.depositInstructions.deposit_message} + {transaction.extraDataForDrawer.depositInstructions.deposit_message.slice( + 0, + 10 + )}
@@ -877,8 +904,32 @@ export const TransactionDetailsReceipt = ({
{/* Collapsible bank details */} + {showBankDetails && ( <> + {transaction.extraDataForDrawer.depositInstructions.account_holder_name && ( + + + { + transaction.extraDataForDrawer.depositInstructions + .account_holder_name + } + + +
+ } + hideBottomBorder={false} + /> + )}
} - hideBottomBorder={false} + hideBottomBorder={true} /> - {transaction.extraDataForDrawer.depositInstructions.account_holder_name && ( - - - { - transaction.extraDataForDrawer.depositInstructions - .account_holder_name - } - - -
- } - hideBottomBorder={true} - /> - )} ) : ( /* US format (Account Number/Routing Number) */ diff --git a/src/components/TransactionDetails/transaction-details.utils.ts b/src/components/TransactionDetails/transaction-details.utils.ts index e1b93915b..8b4af2775 100644 --- a/src/components/TransactionDetails/transaction-details.utils.ts +++ b/src/components/TransactionDetails/transaction-details.utils.ts @@ -7,6 +7,7 @@ export type TransactionDetailsRowKey = | 'txId' | 'cancelled' | 'completed' + | 'refunded' | 'exchangeRate' | 'bankAccountDetails' | 'transferId' @@ -26,6 +27,7 @@ export const transactionDetailsRowKeys: TransactionDetailsRowKey[] = [ 'cancelled', 'claimed', 'completed', + 'refunded', 'closed', 'to', 'tokenAndNetwork', diff --git a/src/components/TransactionDetails/transactionTransformer.ts b/src/components/TransactionDetails/transactionTransformer.ts index 0a271bdc1..9bee557d2 100644 --- a/src/components/TransactionDetails/transactionTransformer.ts +++ b/src/components/TransactionDetails/transactionTransformer.ts @@ -12,7 +12,7 @@ import { } from '@/utils/general.utils' import { type StatusPillType } from '../Global/StatusPill' import type { Address } from 'viem' -import { PEANUT_WALLET_CHAIN } from '@/constants' +import { PEANUT_WALLET_CHAIN } from '@/constants/zerodev.consts' import { type HistoryEntryPerkReward, type ChargeEntry } from '@/services/services.types' /** @@ -436,6 +436,9 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact case 'EXPIRED': uiStatus = 'cancelled' break + case 'REFUNDED': + uiStatus = 'refunded' + break case 'CLOSED': // If the total amount collected is 0, the link is treated as cancelled uiStatus = entry.totalAmountCollected === 0 ? 'cancelled' : 'closed' diff --git a/src/components/User/UserCard.tsx b/src/components/User/UserCard.tsx index e0a96ecb8..a6d156356 100644 --- a/src/components/User/UserCard.tsx +++ b/src/components/User/UserCard.tsx @@ -9,9 +9,11 @@ import AvatarWithBadge, { type AvatarSize } from '../Profile/AvatarWithBadge' import { VerifiedUserLabel } from '../UserHeader' import { twMerge } from 'tailwind-merge' import ProgressBar from '../Global/ProgressBar' +import { ContributorsDrawer } from '@/features/payments/flows/contribute-pot/components/ContributorsDrawer' +import type { PotContributor } from '@/features/payments/flows/contribute-pot/ContributePotFlowContext' interface UserCardProps { - type: 'send' | 'request' | 'received_link' | 'request_pay' + type: 'send' | 'request' | 'received_link' | 'request_pay' | 'request_fulfilment' username: string fullName?: string recipientType?: RecipientType @@ -23,6 +25,7 @@ interface UserCardProps { amount?: number amountCollected?: number isRequestPot?: boolean + contributors?: PotContributor[] } const UserCard = ({ @@ -38,11 +41,13 @@ const UserCard = ({ amount, amountCollected, isRequestPot, + contributors, }: UserCardProps) => { const getIcon = (): IconName | undefined => { if (type === 'send') return 'arrow-up-right' if (type === 'request') return 'arrow-down-left' if (type === 'received_link') return 'arrow-down-left' + if (type === 'request_fulfilment') return 'arrow-up-right' } const getTitle = useCallback(() => { @@ -52,7 +57,7 @@ const UserCard = ({ if (type === 'request') title = `Requesting money from` if (type === 'received_link') title = `You received` if (type === 'request_pay') title = `${fullName ?? username} is requesting` - + if (type === 'request_fulfilment') title = `Sending ${fullName ?? username}` return (
{icon && } {title} @@ -87,16 +92,30 @@ const UserCard = ({ />
{getTitle()} - {recipientType !== 'USERNAME' || type === 'request_pay' ? ( - + {type === 'request_fulfilment' && ( +
+

${amount}

+
+ +

Send the exact amount!

+
+
)} - isLink={type !== 'request_pay'} - /> + + {type !== 'request_fulfilment' && ( + + )} + ) : ( 0 && ( = amount} /> )} + + {/* request pot contributors drawer */} + {isRequestPot && contributors && } ) } diff --git a/src/components/UserHeader/index.tsx b/src/components/UserHeader/index.tsx index 10765347f..ce7e0d0a2 100644 --- a/src/components/UserHeader/index.tsx +++ b/src/components/UserHeader/index.tsx @@ -6,10 +6,10 @@ import { Icon } from '../Global/Icons/Icon' import { twMerge } from 'tailwind-merge' import { Tooltip } from '../Tooltip' import { useMemo } from 'react' -import { isAddress } from 'viem' import { useAuth } from '@/context/authContext' import AddressLink from '../Global/AddressLink' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' +import { isCryptoAddress } from '@/utils/general.utils' interface UserHeaderProps { username: string @@ -79,8 +79,8 @@ export const VerifiedUserLabel = ({ tooltipContent = "This is a verified user and you've sent them money before." } - const isCryptoAddress = useMemo(() => { - return isAddress(username) + const isCryptoAddressComputed = useMemo(() => { + return isCryptoAddress(username) }, [username]) // O(1) lookup in pre-computed Set @@ -90,7 +90,7 @@ export const VerifiedUserLabel = ({ return (
- {isCryptoAddress ? ( + {isCryptoAddressComputed ? ( + * + * + * ``` + */ + +import { useState, useEffect, type ReactNode } from 'react' +import { verifyPeanutUsername } from '@/lib/validation/recipient' +import type { ValidationErrorViewProps } from '@/components/Payment/Views/Error.validation.view' +import ValidationErrorView from '@/components/Payment/Views/Error.validation.view' +import PeanutLoading from '@/components/Global/PeanutLoading' + +interface ValidatedUsernameWrapperProps { + username: string + children: ReactNode + errorProps?: Partial + loadingClassName?: string +} + +export function ValidatedUsernameWrapper({ + username, + children, + errorProps, + loadingClassName = 'flex min-h-[calc(100dvh-180px)] w-full items-center justify-center', +}: ValidatedUsernameWrapperProps) { + const [error, setError] = useState(null) + const [isValidating, setIsValidating] = useState(false) + const [isValidated, setIsValidated] = useState(false) + + // validate username before showing children + useEffect(() => { + let isMounted = true + + const validateUsername = async () => { + setIsValidating(true) + setError(null) + + const isValid = await verifyPeanutUsername(username) + + if (!isMounted) return + + if (!isValid) { + setError({ + title: `We don't know any @${username}`, + message: 'Are you sure you clicked on the right link?', + buttonText: 'Go back to home', + redirectTo: '/home', + showLearnMore: false, + supportMessageTemplate: 'I clicked on this link but got an error: {url}', + ...errorProps, + }) + setIsValidated(false) + } else { + setIsValidated(true) + } + + setIsValidating(false) + } + + validateUsername() + + return () => { + isMounted = false + } + }, [username, errorProps]) + + // show loading while validating + if (isValidating) { + return ( +
+ +
+ ) + } + + // show error if validation failed + if (error) { + return ( +
+ +
+ ) + } + + // show children only after successful validation + if (!isValidated) { + return ( +
+ +
+ ) + } + + return <>{children} +} diff --git a/src/components/Withdraw/views/Confirm.withdraw.view.tsx b/src/components/Withdraw/views/Confirm.withdraw.view.tsx index d556a048a..7c507c3da 100644 --- a/src/components/Withdraw/views/Confirm.withdraw.view.tsx +++ b/src/components/Withdraw/views/Confirm.withdraw.view.tsx @@ -1,6 +1,6 @@ 'use client' -import { Button } from '@/components/0_Bruddle' +import { Button } from '@/components/0_Bruddle/Button' import AddressLink from '@/components/Global/AddressLink' import Card from '@/components/Global/Card' import DisplayIcon from '@/components/Global/DisplayIcon' @@ -10,12 +10,12 @@ import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow' import { useTokenChainIcons } from '@/hooks/useTokenChainIcons' import { type ITokenPriceData } from '@/interfaces' -import { formatAmount, isStableCoin } from '@/utils' +import { formatAmount, isStableCoin } from '@/utils/general.utils' import { interfaces } from '@squirrel-labs/peanut-sdk' import { type PeanutCrossChainRoute } from '@/services/swap' import { useMemo, useState } from 'react' import { formatUnits } from 'viem' -import { ROUTE_NOT_FOUND_ERROR } from '@/constants' +import { ROUTE_NOT_FOUND_ERROR } from '@/constants/general.consts' interface WithdrawConfirmViewProps { amount: string diff --git a/src/components/Withdraw/views/Initial.withdraw.view.tsx b/src/components/Withdraw/views/Initial.withdraw.view.tsx index a7f51f7a1..5ee4b5b56 100644 --- a/src/components/Withdraw/views/Initial.withdraw.view.tsx +++ b/src/components/Withdraw/views/Initial.withdraw.view.tsx @@ -5,7 +5,6 @@ import ErrorAlert from '@/components/Global/ErrorAlert' import GeneralRecipientInput, { type GeneralRecipientUpdate } from '@/components/Global/GeneralRecipientInput' import NavHeader from '@/components/Global/NavHeader' import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard' -import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants' import { useWithdrawFlow } from '@/context/WithdrawFlowContext' import { tokenSelectorContext } from '@/context/tokenSelector.context' import { type ITokenPriceData } from '@/interfaces' @@ -14,6 +13,7 @@ import { interfaces } from '@squirrel-labs/peanut-sdk' import { useRouter } from 'next/navigation' import { useContext, useEffect } from 'react' import TokenSelector from '@/components/Global/TokenSelector/TokenSelector' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants/zerodev.consts' interface InitialWithdrawViewProps { amount: string diff --git a/src/components/utils/utils.ts b/src/components/utils/utils.ts index 034fd3f8b..0fc744b35 100644 --- a/src/components/utils/utils.ts +++ b/src/components/utils/utils.ts @@ -1,6 +1,6 @@ -import * as consts from '@/constants' -import { fetchWithSentry, getExplorerUrl } from '@/utils' -import * as Sentry from '@sentry/nextjs' +import { SQUID_API_URL, SQUID_INTEGRATOR_ID } from '@/constants/general.consts' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { getExplorerUrl } from '@/utils/general.utils' type ISquidChainData = { id: string @@ -70,8 +70,8 @@ type ISquidStatusResponse = { export async function checkTransactionStatus(txHash: string): Promise { try { - const response = await fetchWithSentry(`${consts.SQUID_API_URL}/status?transactionId=${txHash}`, { - headers: { 'x-integrator-id': consts.SQUID_INTEGRATOR_ID }, // TODO: request v2 removes checking squid status + const response = await fetchWithSentry(`${SQUID_API_URL}/status?transactionId=${txHash}`, { + headers: { 'x-integrator-id': SQUID_INTEGRATOR_ID }, // TODO: request v2 removes checking squid status }) const data = await response.json() return data diff --git a/src/config/peanut.config.tsx b/src/config/peanut.config.tsx index f663bd95a..79cef901b 100644 --- a/src/config/peanut.config.tsx +++ b/src/config/peanut.config.tsx @@ -9,20 +9,17 @@ import { Provider as ReduxProvider } from 'react-redux' import store from '@/redux/store' import 'react-tooltip/dist/react-tooltip.css' -import '../../sentry.client.config' -import '../../sentry.edge.config' -import '../../sentry.server.config' +// Note: Sentry configs are auto-loaded by @sentry/nextjs via next.config.js +// DO NOT import them here - it bundles server/edge configs into client code export function PeanutProvider({ children }: { children: React.ReactNode }) { - if (process.env.NODE_ENV !== 'development') { - useEffect(() => { + useEffect(() => { + if (process.env.NODE_ENV !== 'development') { peanut.toggleVerbose(true) // LogRocket.init('x2zwq1/peanut-protocol') countries.registerLocale(enLocale) - }, []) - } - - console.log('NODE_ENV:', process.env.NODE_ENV) + } + }, []) return ( diff --git a/src/config/theme.config.tsx b/src/config/theme.config.tsx index 8e85b4984..1d6791fa7 100644 --- a/src/config/theme.config.tsx +++ b/src/config/theme.config.tsx @@ -1,14 +1,7 @@ 'use client' -import { theme } from '@/styles/theme' -import { ColorModeProvider, ColorModeScript } from '@chakra-ui/color-mode' -import { ChakraProvider } from '@chakra-ui/react' +import { ThemeProvider as MuiThemeProvider, createTheme } from '@mui/material/styles' export const ThemeProvider = ({ children }: { children: React.ReactNode }) => { - return ( - - - {children} - - ) + return {children} } diff --git a/src/constants/countryCurrencyMapping.ts b/src/constants/countryCurrencyMapping.ts index 3af5d0e37..26c926cca 100644 --- a/src/constants/countryCurrencyMapping.ts +++ b/src/constants/countryCurrencyMapping.ts @@ -7,7 +7,7 @@ export interface CountryCurrencyMapping { path?: string } -export const countryCurrencyMappings: CountryCurrencyMapping[] = [ +const countryCurrencyMappings: CountryCurrencyMapping[] = [ // SEPA Countries (Eurozone) { currencyCode: 'EUR', currencyName: 'Euro', country: 'Eurozone', flagCode: 'eu' }, diff --git a/src/constants/index.ts b/src/constants/index.ts deleted file mode 100644 index 64edf849b..000000000 --- a/src/constants/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export * from './cashout.consts' -export * from './chains.consts' -export * from './carousel.consts' -export * from './general.consts' -export * from './loadingStates.consts' -export * from './query.consts' -export * from './zerodev.consts' -export * from './manteca.consts' -export * from './payment.consts' -export * from './routes' -export * from './stateCodes.consts' -export * from './tweets.consts' diff --git a/src/constants/payment.consts.ts b/src/constants/payment.consts.ts index 7d62bd236..49b09ddff 100644 --- a/src/constants/payment.consts.ts +++ b/src/constants/payment.consts.ts @@ -10,12 +10,6 @@ export const MIN_MANTECA_DEPOSIT_AMOUNT = 1 // QR payment limits for manteca (PIX, MercadoPago, QR3) export const MIN_MANTECA_QR_PAYMENT_AMOUNT = 0.1 // Manteca provider minimum -// time constants for devconnect intent cleanup -export const DEVCONNECT_INTENT_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000 // 7 days - -// maximum number of devconnect intents to store per user -export const MAX_DEVCONNECT_INTENTS = 10 - /** * validate if amount meets minimum requirement for a payment method * @param amount - amount in USD diff --git a/src/constants/rhino.consts.ts b/src/constants/rhino.consts.ts new file mode 100644 index 000000000..a99899f88 --- /dev/null +++ b/src/constants/rhino.consts.ts @@ -0,0 +1,59 @@ +/** Chain name to logo URL mapping - reusable across the app */ +export const CHAIN_LOGOS = { + ARBITRUM: 'https://assets.coingecko.com/asset_platforms/images/33/standard/AO_logomark.png?1706606717', + ETHEREUM: 'https://assets.coingecko.com/asset_platforms/images/279/standard/ethereum.png?1706606803', + BASE: 'https://assets.coingecko.com/asset_platforms/images/131/standard/base.png?1759905869', + OPTIMISM: 'https://assets.coingecko.com/asset_platforms/images/41/standard/optimism.png?1706606778', + GNOSIS: 'https://assets.coingecko.com/asset_platforms/images/11062/standard/Aatar_green_white.png?1706606458', + POLYGON: 'https://assets.coingecko.com/asset_platforms/images/15/standard/polygon_pos.png?1706606645', + BNB: 'https://assets.coingecko.com/asset_platforms/images/1/standard/bnb_smart_chain.png?1706606721', + KATANA: 'https://assets.coingecko.com/asset_platforms/images/32239/standard/katana.jpg?1751496126', + SCROLL: 'https://assets.coingecko.com/asset_platforms/images/153/standard/scroll.jpeg?1706606782', + CELO: 'https://assets.coingecko.com/asset_platforms/images/21/standard/celo.jpeg?1711358666', + TRON: 'https://assets.coingecko.com/asset_platforms/images/1094/standard/TRON_LOGO.png?1706606652', + SOLANA: 'https://assets.coingecko.com/asset_platforms/images/5/standard/solana.png?1706606708', +} as const + +/** Token symbol to logo URL mapping - reusable across the app */ +export const TOKEN_LOGOS = { + USDT: 'https://assets.coingecko.com/coins/images/325/standard/Tether.png?1696501661', + USDC: 'https://assets.coingecko.com/coins/images/6319/small/USD_Coin_icon.png', +} as const + +export type ChainName = keyof typeof CHAIN_LOGOS +export type TokenName = keyof typeof TOKEN_LOGOS + +export const SUPPORTED_EVM_CHAINS = [ + 'ARBITRUM', + 'ETHEREUM', + 'BASE', + 'OPTIMISM', + 'BNB', + 'POLYGON', + 'KATANA', + 'SCROLL', + 'GNOSIS', + 'CELO', +] as const + +export const OTHER_SUPPORTED_CHAINS = ['SOLANA', 'TRON'] as const + +/** Rhino-supported chains with their logos */ +export const RHINO_SUPPORTED_CHAINS = (Object.keys(CHAIN_LOGOS) as ChainName[]).map((name) => ({ + name, + logoUrl: CHAIN_LOGOS[name], +})) + +export const RHINO_SUPPORTED_EVM_CHAINS = RHINO_SUPPORTED_CHAINS.filter((chain) => + (SUPPORTED_EVM_CHAINS as readonly string[]).includes(chain.name) +) + +export const RHINO_SUPPORTED_OTHER_CHAINS = RHINO_SUPPORTED_CHAINS.filter((chain) => + (OTHER_SUPPORTED_CHAINS as readonly string[]).includes(chain.name) +) + +/** Rhino-supported tokens with their logos */ +export const RHINO_SUPPORTED_TOKENS = (Object.keys(TOKEN_LOGOS) as TokenName[]).map((name) => ({ + name, + logoUrl: TOKEN_LOGOS[name], +})) diff --git a/src/constants/zerodev.consts.ts b/src/constants/zerodev.consts.ts index ce9ce65a6..5e9c9b4c4 100644 --- a/src/constants/zerodev.consts.ts +++ b/src/constants/zerodev.consts.ts @@ -1,8 +1,5 @@ import { getEntryPoint, KERNEL_V3_1 } from '@zerodev/sdk/constants' -import type { Chain, PublicClient } from 'viem' -import { createPublicClient } from 'viem' -import { getTransportWithFallback } from '@/app/actions/clients' -import { arbitrum, mainnet, base, linea } from 'viem/chains' +import { arbitrum } from 'viem/chains' // consts needed to define low level SDK kernel // as per: https://docs.zerodev.app/sdk/getting-started/tutorial-passkeys @@ -38,81 +35,3 @@ export const PEANUT_WALLET_SUPPORTED_TOKENS: Record = { export const USER_OP_ENTRY_POINT = getEntryPoint('0.7') export const ZERODEV_KERNEL_VERSION = KERNEL_V3_1 export const USER_OPERATION_REVERT_REASON_TOPIC = '0x1c4fada7374c0a9ee8841fc38afe82932dc0f8e69012e927f061a8bae611a201' - -const ZERODEV_V3_URL = process.env.NEXT_PUBLIC_ZERO_DEV_RECOVERY_BUNDLER_URL -const zerodevV3Url = (chainId: number | string) => `${ZERODEV_V3_URL}/chain/${chainId}` - -/** - * This is a mapping of chain ID to the public client and chain details - * This is for the standard chains supported in the app. Arbitrum is always included - * as it's the primary wallet chain. Additional chains (mainnet, base, linea) are only - * included if NEXT_PUBLIC_ZERO_DEV_RECOVERY_BUNDLER_URL is configured. - */ -export const PUBLIC_CLIENTS_BY_CHAIN: Record< - string, - { - client: PublicClient - chain: Chain - bundlerUrl: string - paymasterUrl: string - } -> = { - // Arbitrum (primary wallet chain - always included) - [arbitrum.id]: { - client: createPublicClient({ - transport: getTransportWithFallback(arbitrum.id), - chain: arbitrum, - pollingInterval: 500, - }), - chain: PEANUT_WALLET_CHAIN, - bundlerUrl: BUNDLER_URL, - paymasterUrl: PAYMASTER_URL, - }, -} - -// Conditionally add recovery chains if env var is configured -if (ZERODEV_V3_URL) { - const mainnetUrl = zerodevV3Url(mainnet.id) - if (mainnetUrl) { - PUBLIC_CLIENTS_BY_CHAIN[mainnet.id] = { - client: createPublicClient({ - transport: getTransportWithFallback(mainnet.id), - chain: mainnet, - pollingInterval: 12000, - }), - chain: mainnet, - bundlerUrl: mainnetUrl, - paymasterUrl: mainnetUrl, - } - } - - const baseUrl = zerodevV3Url(base.id) - if (baseUrl) { - PUBLIC_CLIENTS_BY_CHAIN[base.id] = { - client: createPublicClient({ - transport: getTransportWithFallback(base.id), - chain: base, - pollingInterval: 2000, - }) as PublicClient, - chain: base, - bundlerUrl: baseUrl, - paymasterUrl: baseUrl, - } - } - - const lineaUrl = zerodevV3Url(linea.id) - if (lineaUrl) { - PUBLIC_CLIENTS_BY_CHAIN[linea.id] = { - client: createPublicClient({ - transport: getTransportWithFallback(linea.id), - chain: linea, - pollingInterval: 3000, - }), - chain: linea, - bundlerUrl: lineaUrl, - paymasterUrl: lineaUrl, - } - } -} - -export const peanutPublicClient = PUBLIC_CLIENTS_BY_CHAIN[PEANUT_WALLET_CHAIN.id].client diff --git a/src/context/LinkSendFlowContext.tsx b/src/context/LinkSendFlowContext.tsx new file mode 100644 index 000000000..2887b1095 --- /dev/null +++ b/src/context/LinkSendFlowContext.tsx @@ -0,0 +1,111 @@ +'use client' + +/** + * context for send link flow state management + * + * send links let users create claimable payment links that can be: + * - shared via any messaging app + * - claimed by anyone with the link + * - cross-chain claimable + * + * this context manages state for creating these links + */ + +import { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from 'react' +import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' + +// view states for link send flow +export type LinkSendFlowView = 'INITIAL' | 'SUCCESS' + +// attachment options for link (matches IAttachmentOptions from shared types) +export interface LinkSendAttachmentOptions { + fileUrl: string | undefined + message: string | undefined + rawFile: File | undefined +} + +// error state +export interface LinkSendErrorState { + showError: boolean + errorMessage: string +} + +// context type +interface LinkSendFlowContextType { + // state + view: LinkSendFlowView + setView: (view: LinkSendFlowView) => void + tokenValue: string | undefined + setTokenValue: (value: string | undefined) => void + link: string | undefined + setLink: (link: string | undefined) => void + attachmentOptions: LinkSendAttachmentOptions + setAttachmentOptions: (options: LinkSendAttachmentOptions) => void + errorState: LinkSendErrorState | undefined + setErrorState: (state: LinkSendErrorState | undefined) => void + crossChainDetails: peanutInterfaces.ISquidChain[] | undefined + setCrossChainDetails: (details: peanutInterfaces.ISquidChain[] | undefined) => void + + // actions + resetLinkSendFlow: () => void +} + +const LinkSendFlowContext = createContext(undefined) + +interface LinkSendFlowProviderProps { + children: ReactNode +} + +export const LinkSendFlowProvider: React.FC = ({ children }) => { + const [view, setView] = useState('INITIAL') + const [tokenValue, setTokenValue] = useState(undefined) + const [link, setLink] = useState(undefined) + const [attachmentOptions, setAttachmentOptions] = useState({ + fileUrl: undefined, + message: undefined, + rawFile: undefined, + }) + const [errorState, setErrorState] = useState(undefined) + const [crossChainDetails, setCrossChainDetails] = useState(undefined) + + const resetLinkSendFlow = useCallback(() => { + setView('INITIAL') + setTokenValue(undefined) + setLink(undefined) + setAttachmentOptions({ + fileUrl: undefined, + message: undefined, + rawFile: undefined, + }) + setErrorState(undefined) + }, []) + + const value = useMemo( + () => ({ + view, + setView, + tokenValue, + setTokenValue, + link, + setLink, + attachmentOptions, + setAttachmentOptions, + errorState, + setErrorState, + crossChainDetails, + setCrossChainDetails, + resetLinkSendFlow, + }), + [view, tokenValue, link, attachmentOptions, errorState, crossChainDetails, resetLinkSendFlow] + ) + + return {children} +} + +export const useLinkSendFlow = (): LinkSendFlowContextType => { + const context = useContext(LinkSendFlowContext) + if (context === undefined) { + throw new Error('useLinkSendFlow must be used within LinkSendFlowProvider') + } + return context +} diff --git a/src/context/ModalsContext.tsx b/src/context/ModalsContext.tsx index 8658d6f63..98c237028 100644 --- a/src/context/ModalsContext.tsx +++ b/src/context/ModalsContext.tsx @@ -1,27 +1,81 @@ 'use client' -import { createContext, useContext, useState, type ReactNode } from 'react' +import { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from 'react' interface ModalsContextType { + // iOS PWA Install Modal isIosPwaInstallModalOpen: boolean setIsIosPwaInstallModalOpen: (isOpen: boolean) => void + + // Guest Login/Sign In Modal + isSignInModalOpen: boolean + setIsSignInModalOpen: (isOpen: boolean) => void + + // Support Drawer + isSupportModalOpen: boolean + setIsSupportModalOpen: (isOpen: boolean) => void + supportPrefilledMessage: string + setSupportPrefilledMessage: (message: string) => void + openSupportWithMessage: (message: string) => void + + // QR Scanner + isQRScannerOpen: boolean + setIsQRScannerOpen: (isOpen: boolean) => void } const ModalsContext = createContext(undefined) export function ModalsProvider({ children }: { children: ReactNode }) { + // iOS PWA Install Modal const [isIosPwaInstallModalOpen, setIsIosPwaInstallModalOpen] = useState(false) - return ( - - {children} - + // Guest Login/Sign In Modal + const [isSignInModalOpen, setIsSignInModalOpen] = useState(false) + + // Support Drawer + const [isSupportModalOpen, setIsSupportModalOpen] = useState(false) + const [supportPrefilledMessage, setSupportPrefilledMessage] = useState('') + + // QR Scanner + const [isQRScannerOpen, setIsQRScannerOpen] = useState(false) + + const openSupportWithMessage = useCallback((message: string) => { + setSupportPrefilledMessage(message) + setIsSupportModalOpen(true) + }, []) + + const value = useMemo( + () => ({ + // iOS PWA Install Modal + isIosPwaInstallModalOpen, + setIsIosPwaInstallModalOpen, + + // Guest Login/Sign In Modal + isSignInModalOpen, + setIsSignInModalOpen, + + // Support Drawer + isSupportModalOpen, + setIsSupportModalOpen, + supportPrefilledMessage, + setSupportPrefilledMessage, + openSupportWithMessage, + + // QR Scanner + isQRScannerOpen, + setIsQRScannerOpen, + }), + [ + isIosPwaInstallModalOpen, + isSignInModalOpen, + isSupportModalOpen, + supportPrefilledMessage, + openSupportWithMessage, + isQRScannerOpen, + ] ) + + return {children} } export function useModalsContext() { diff --git a/src/context/QrCodeContext.tsx b/src/context/QrCodeContext.tsx deleted file mode 100644 index 53c740f71..000000000 --- a/src/context/QrCodeContext.tsx +++ /dev/null @@ -1,23 +0,0 @@ -'use client' - -import { createContext, useContext, useState, type ReactNode } from 'react' - -interface QrCodeContextType { - isQRScannerOpen: boolean - setIsQRScannerOpen: (isOpen: boolean) => void -} - -const QrCodeContext = createContext(undefined) - -export function QrCodeProvider({ children }: { children: ReactNode }) { - const [isQRScannerOpen, setIsQRScannerOpen] = useState(false) - return {children} -} - -export function useQrCodeContext() { - const context = useContext(QrCodeContext) - if (context === undefined) { - throw new Error('useQrCodeContext must be used within a QrCodeProvider') - } - return context -} diff --git a/src/context/SupportModalContext.tsx b/src/context/SupportModalContext.tsx deleted file mode 100644 index 1ef2ca6e3..000000000 --- a/src/context/SupportModalContext.tsx +++ /dev/null @@ -1,45 +0,0 @@ -'use client' - -import { createContext, useContext, useState, type ReactNode } from 'react' - -interface SupportModalContextType { - isSupportModalOpen: boolean - setIsSupportModalOpen: (isOpen: boolean) => void - prefilledMessage: string - setPrefilledMessage: (message: string) => void - openSupportWithMessage: (message: string) => void -} - -const SupportModalContext = createContext(undefined) - -export function SupportModalProvider({ children }: { children: ReactNode }) { - const [isSupportModalOpen, setIsSupportModalOpen] = useState(false) - const [prefilledMessage, setPrefilledMessage] = useState('') - - const openSupportWithMessage = (message: string) => { - setPrefilledMessage(message) - setIsSupportModalOpen(true) - } - - return ( - - {children} - - ) -} - -export function useSupportModalContext() { - const context = useContext(SupportModalContext) - if (context === undefined) { - throw new Error('useSupportModal must be used within a SupportModalProvider') - } - return context -} diff --git a/src/context/WithdrawFlowContext.tsx b/src/context/WithdrawFlowContext.tsx index 7898bf0d0..aa594e65b 100644 --- a/src/context/WithdrawFlowContext.tsx +++ b/src/context/WithdrawFlowContext.tsx @@ -1,6 +1,7 @@ 'use client' import { type ITokenPriceData, type Account } from '@/interfaces' +import { type TRequestChargeResponse, type PaymentCreationResponse } from '@/services/services.types' import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' import React, { createContext, type ReactNode, useContext, useMemo, useState, useCallback } from 'react' @@ -61,6 +62,13 @@ interface WithdrawFlowContextType { setShowAllWithdrawMethods: (show: boolean) => void selectedMethod: WithdrawMethod | null setSelectedMethod: (method: WithdrawMethod | null) => void + // charge and payment state (local to withdraw flow) + chargeDetails: TRequestChargeResponse | null + setChargeDetails: (charge: TRequestChargeResponse | null) => void + transactionHash: string | null + setTransactionHash: (hash: string | null) => void + paymentDetails: PaymentCreationResponse | null + setPaymentDetails: (payment: PaymentCreationResponse | null) => void resetWithdrawFlow: () => void } @@ -86,6 +94,11 @@ export const WithdrawFlowContextProvider: React.FC<{ children: ReactNode }> = ({ const [showAllWithdrawMethods, setShowAllWithdrawMethods] = useState(false) const [selectedMethod, setSelectedMethod] = useState(null) + // charge and payment state (local to withdraw flow) + const [chargeDetails, setChargeDetails] = useState(null) + const [transactionHash, setTransactionHash] = useState(null) + const [paymentDetails, setPaymentDetails] = useState(null) + const resetWithdrawFlow = useCallback(() => { setAmountToWithdraw('') setCurrentView('INITIAL') @@ -97,6 +110,10 @@ export const WithdrawFlowContextProvider: React.FC<{ children: ReactNode }> = ({ setShowAllWithdrawMethods(false) setUsdAmount('') setSelectedMethod(null) + // reset charge and payment state + setChargeDetails(null) + setTransactionHash(null) + setPaymentDetails(null) }, []) const value = useMemo( @@ -129,6 +146,12 @@ export const WithdrawFlowContextProvider: React.FC<{ children: ReactNode }> = ({ setShowAllWithdrawMethods, selectedMethod, setSelectedMethod, + chargeDetails, + setChargeDetails, + transactionHash, + setTransactionHash, + paymentDetails, + setPaymentDetails, resetWithdrawFlow, }), [ @@ -146,7 +169,9 @@ export const WithdrawFlowContextProvider: React.FC<{ children: ReactNode }> = ({ selectedBankAccount, showAllWithdrawMethods, selectedMethod, - setShowAllWithdrawMethods, + chargeDetails, + transactionHash, + paymentDetails, resetWithdrawFlow, ] ) diff --git a/src/context/authContext.tsx b/src/context/authContext.tsx index 52aa33bd1..43139b0fc 100644 --- a/src/context/authContext.tsx +++ b/src/context/authContext.tsx @@ -6,12 +6,12 @@ import { useAppDispatch } from '@/redux/hooks' import { setupActions } from '@/redux/slices/setup-slice' import { zerodevActions } from '@/redux/slices/zerodev-slice' import { - fetchWithSentry, removeFromCookie, syncLocalStorageToCookie, clearRedirectUrl, updateUserPreferences, -} from '@/utils' +} from '@/utils/general.utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { resetCrispProxySessions } from '@/utils/crisp' import { useQueryClient } from '@tanstack/react-query' import { useRouter } from 'next/navigation' diff --git a/src/context/contextProvider.tsx b/src/context/contextProvider.tsx index 2b142af28..cdd9f4f49 100644 --- a/src/context/contextProvider.tsx +++ b/src/context/contextProvider.tsx @@ -3,43 +3,37 @@ import { OnrampFlowContextProvider } from './OnrampFlowContext' import { AuthProvider } from './authContext' import { KernelClientProvider } from './kernelClient.context' import { LoadingStateContextProvider } from './loadingStates.context' -import { PushProvider } from './pushProvider' import { TokenContextProvider } from './tokenSelector.context' import { WithdrawFlowContextProvider } from './WithdrawFlowContext' import { ClaimBankFlowContextProvider } from './ClaimBankFlowContext' import { RequestFulfilmentFlowContextProvider } from './RequestFulfillmentFlowContext' -import { SupportModalProvider } from './SupportModalContext' import { PasskeySupportProvider } from './passkeySupportContext' -import { QrCodeProvider } from './QrCodeContext' import { ModalsProvider } from './ModalsContext' +// note: push notifications are now handled by onesignal via useNotifications hook. +// the legacy PushProvider (web-push based) has been removed. + export const ContextProvider = ({ children }: { children: React.ReactNode }) => { return ( - - - - - - - - - - - - {children} - - - - - - - - - - - + + + + + + + + + {children} + + + + + + + + ) diff --git a/src/context/kernelClient.context.tsx b/src/context/kernelClient.context.tsx index 4524727b8..66f8f145c 100644 --- a/src/context/kernelClient.context.tsx +++ b/src/context/kernelClient.context.tsx @@ -1,15 +1,10 @@ 'use client' -import { - PEANUT_WALLET_CHAIN, - PUBLIC_CLIENTS_BY_CHAIN, - USER_OP_ENTRY_POINT, - ZERODEV_KERNEL_VERSION, -} from '@/constants/zerodev.consts' +import { PEANUT_WALLET_CHAIN, USER_OP_ENTRY_POINT, ZERODEV_KERNEL_VERSION } from '@/constants/zerodev.consts' import { useAuth } from '@/context/authContext' import { createKernelMigrationAccount } from '@zerodev/sdk/accounts' import { useAppDispatch } from '@/redux/hooks' import { zerodevActions } from '@/redux/slices/zerodev-slice' -import { getFromCookie, updateUserPreferences, getUserPreferences } from '@/utils' +import { getFromCookie, updateUserPreferences, getUserPreferences } from '@/utils/general.utils' import { PasskeyValidatorContractVersion, toPasskeyValidator, toWebAuthnKey } from '@zerodev/passkey-validator' import { createKernelAccount, @@ -21,6 +16,7 @@ import { createContext, type ReactNode, useCallback, useContext, useEffect, useS import { type Chain, http, type PublicClient, type Transport } from 'viem' import type { Address } from 'viem' import { captureException } from '@sentry/nextjs' +import { PUBLIC_CLIENTS_BY_CHAIN } from '@/app/actions/clients' interface KernelClientContextType { setWebAuthnKey: (webAuthnKey: WebAuthnKey) => void diff --git a/src/context/loadingStates.context.tsx b/src/context/loadingStates.context.tsx index cf683e7db..6329de93f 100644 --- a/src/context/loadingStates.context.tsx +++ b/src/context/loadingStates.context.tsx @@ -1,10 +1,10 @@ 'use client' -import React, { createContext, useContext, useMemo, useState } from 'react' +import React, { createContext, useMemo, useState } from 'react' -import * as consts from '@/constants' +import type { LoadingStates } from '@/constants/loadingStates.consts' export const loadingStateContext = createContext({ - loadingState: '' as consts.LoadingStates, - setLoadingState: (state: consts.LoadingStates) => {}, + loadingState: '' as LoadingStates, + setLoadingState: (state: LoadingStates) => {}, isLoading: false as boolean, }) @@ -14,7 +14,7 @@ export const loadingStateContext = createContext({ * Used for all loading states; e.g., fetching data, processing transactions, switching chains... */ export const LoadingStateContextProvider = ({ children }: { children: React.ReactNode }) => { - const [loadingState, setLoadingState] = useState('Idle') + const [loadingState, setLoadingState] = useState('Idle') const isLoading = useMemo(() => loadingState !== 'Idle', [loadingState]) return ( diff --git a/src/context/pushProvider.tsx b/src/context/pushProvider.tsx deleted file mode 100644 index 61663e41f..000000000 --- a/src/context/pushProvider.tsx +++ /dev/null @@ -1,163 +0,0 @@ -'use client' - -import { sendNotification, subscribeUser } from '@/app/actions' -import { useToast } from '@/components/0_Bruddle/Toast' -import { urlBase64ToUint8Array } from '@/utils' -import { captureException } from '@sentry/nextjs' -import React, { createContext, useContext, useEffect, useState } from 'react' -import webpush from 'web-push' -import { useAuth } from './authContext' - -interface PushContextType { - subscribe: () => Promise - unsubscribe: () => void - isSupported: boolean - isSubscribing: boolean - isSubscribed: boolean - send: ({ message, title }: { message: string; title: string }) => void -} - -const PushContext = createContext(undefined) - -export function PushProvider({ children }: { children: React.ReactNode }) { - const toast = useToast() - const { userId } = useAuth() - const [isSupported, setIsSupported] = useState(false) - const [isSubscribed, setIsSubscribed] = useState(false) - const [isSubscribing, setIsSubscribing] = useState(false) - const [registration, setRegistration] = useState(null) - const [subscription, setSubscription] = useState(null) - - const registerServiceWorker = async () => { - console.log('[PushProvider] Getting service worker registration') - try { - // Use existing SW registration (registered in layout.tsx for offline support) - // navigator.serviceWorker.ready waits for SW to be registered and active - // Timeout after 10s to prevent infinite wait if SW registration fails - const reg = (await Promise.race([ - navigator.serviceWorker.ready, - new Promise((_, reject) => setTimeout(() => reject(new Error('SW registration timeout')), 10000)), - ])) as ServiceWorkerRegistration - - console.log('[PushProvider] SW already registered:', reg.scope) - setRegistration(reg) - const sub = await reg.pushManager.getSubscription() - - console.log('[PushProvider] Push subscription:', sub) - - if (sub) { - // @ts-ignore - setSubscription(sub) - setIsSubscribed(true) - } - } catch (error) { - console.error('[PushProvider] Failed to get SW registration:', error) - captureException(error) - // toast.error('Failed to initialize notifications') - } - } - - useEffect(() => { - console.log('Checking for service worker and push manager') - if ('serviceWorker' in navigator && 'PushManager' in window) { - console.log('Service Worker and Push Manager are supported') - setIsSupported(true) - // Wait for service worker to be ready - navigator.serviceWorker.ready - .then(() => { - registerServiceWorker() - }) - .catch((error) => { - console.error('Service Worker not ready:', error) - captureException(error) - // toast.error('Failed to initialize notifications') - }) - } else { - console.log('Service Worker and Push Manager are not supported') - setIsSupported(false) - } - }, []) - - const subscribe = async (): Promise => { - if (!registration) { - toast.error('Something went wrong while initializing notifications') - return - } else if (!userId) { - toast.error('Something went wrong while initializing notifications') - return - } - - setIsSubscribing(true) - try { - const sub = await registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!), - }) - - // @ts-ignore - setSubscription(sub) - - const plainSub = { - endpoint: sub.endpoint, - keys: { - p256dh: Buffer.from((sub as any).getKey('p256dh')).toString('base64'), - auth: Buffer.from((sub as any).getKey('auth')).toString('base64'), - }, - } - - await subscribeUser(userId, plainSub) - - setIsSubscribed(true) - } catch (error: unknown) { - if (error instanceof Error) { - console.log(error.message) - if (error.message.includes('permission denied')) { - toast.error('Please allow notifications in your browser settings') - } else { - toast.error('Failed to enable notifications') - captureException(error) - } - console.error('Error subscribing to push notifications:', error.message) - } else { - throw error - } - } - setIsSubscribing(false) - } - const unsubscribe = async () => {} - - const send = async ({ message, title }: { message: string; title: string }) => { - const plainSub = { - endpoint: subscription!.endpoint, - keys: { - p256dh: Buffer.from((subscription as any).getKey('p256dh')).toString('base64'), - auth: Buffer.from((subscription as any).getKey('auth')).toString('base64'), - }, - } - - await sendNotification(plainSub, { message, title }) - } - - return ( - - {children} - - ) -} - -export function usePush() { - const context = useContext(PushContext) - if (context === undefined) { - throw new Error('usePush must be used within a PushProvider') - } - return context -} diff --git a/src/context/tokenSelector.context.tsx b/src/context/tokenSelector.context.tsx index 4761676db..c0425cc8f 100644 --- a/src/context/tokenSelector.context.tsx +++ b/src/context/tokenSelector.context.tsx @@ -1,5 +1,5 @@ 'use client' -import React, { createContext, useState, useCallback, useMemo, useEffect } from 'react' +import React, { createContext, useState, useCallback, useEffect } from 'react' import { PEANUT_WALLET_CHAIN, @@ -8,7 +8,7 @@ import { PEANUT_WALLET_TOKEN_IMG_URL, PEANUT_WALLET_TOKEN_NAME, PEANUT_WALLET_TOKEN_SYMBOL, -} from '@/constants' +} from '@/constants/zerodev.consts' import { useWallet } from '@/hooks/wallet/useWallet' import { useSquidChainsAndTokens } from '@/hooks/useSquidChainsAndTokens' import { useTokenPrice } from '@/hooks/useTokenPrice' diff --git a/src/features/payments/flows/contribute-pot/ContributePotFlowContext.tsx b/src/features/payments/flows/contribute-pot/ContributePotFlowContext.tsx new file mode 100644 index 000000000..8f99a6c1c --- /dev/null +++ b/src/features/payments/flows/contribute-pot/ContributePotFlowContext.tsx @@ -0,0 +1,249 @@ +'use client' + +/** + * context provider for request pot flow + * + * request pots are group payment requests where multiple people can contribute. + * this context manages all state for the contribution flow: + * - amount being contributed + * - request details and recipient info + * - charge/payment results after execution + * - derived data like total collected and contributors list + * + * wraps ContributePotPage and provides state to all child views + */ + +import { createContext, useContext, useState, useMemo, useCallback, type ReactNode } from 'react' +import { type Address, type Hash } from 'viem' +import { type TRequestResponse, type ChargeEntry, type PaymentCreationResponse } from '@/services/services.types' + +// view states for contribute pot flow +export type ContributePotFlowView = 'INITIAL' | 'STATUS' | 'EXTERNAL_WALLET' + +// recipient info derived from request +export interface PotRecipient { + username: string + address: Address + userId?: string + fullName?: string +} + +// contributor info from charges +export interface PotContributor { + uuid: string + username?: string + address?: string + amount: string + createdAt: string +} + +// attachment state +interface ContributePotAttachment { + message?: string + file?: File + fileUrl?: string +} + +// error state +interface ContributePotError { + showError: boolean + errorMessage: string +} + +// context value type +interface ContributePotFlowContextValue { + // view state + currentView: ContributePotFlowView + setCurrentView: (view: ContributePotFlowView) => void + + // request data (fetched from api) + request: TRequestResponse | null + setRequest: (request: TRequestResponse | null) => void + + // derived recipient + recipient: PotRecipient | null + + // amount state + amount: string + setAmount: (amount: string) => void + usdAmount: string + setUsdAmount: (amount: string) => void + + // attachment state + attachment: ContributePotAttachment + setAttachment: (attachment: ContributePotAttachment) => void + + // charge and payment results + charge: ChargeEntry | null + setCharge: (charge: ChargeEntry | null) => void + payment: PaymentCreationResponse | null + setPayment: (payment: PaymentCreationResponse | null) => void + txHash: Hash | null + setTxHash: (hash: Hash | null) => void + isExternalWalletPayment: boolean + setIsExternalWalletPayment: (isExternalWalletPayment: boolean) => void + + // ui state + error: ContributePotError + setError: (error: ContributePotError) => void + isLoading: boolean + setIsLoading: (loading: boolean) => void + isSuccess: boolean + setIsSuccess: (success: boolean) => void + + // derived data + totalAmount: number + totalCollected: number + contributors: PotContributor[] + + // actions + resetContributePotFlow: () => void +} + +const ContributePotFlowContext = createContext(null) + +interface ContributePotFlowProviderProps { + children: ReactNode + initialRequest: TRequestResponse +} + +export function ContributePotFlowProvider({ children, initialRequest }: ContributePotFlowProviderProps) { + // view state + const [currentView, setCurrentView] = useState('INITIAL') + + // request data + const [request, setRequest] = useState(initialRequest) + + // amount state + const [amount, setAmount] = useState('') + const [usdAmount, setUsdAmount] = useState('') + + // attachment state + const [attachment, setAttachment] = useState({}) + + // charge and payment results + const [charge, setCharge] = useState(null) + const [payment, setPayment] = useState(null) + const [txHash, setTxHash] = useState(null) + const [isExternalWalletPayment, setIsExternalWalletPayment] = useState(false) + + // ui state + const [error, setError] = useState({ showError: false, errorMessage: '' }) + const [isLoading, setIsLoading] = useState(false) + const [isSuccess, setIsSuccess] = useState(false) + + // derive recipient from request + const recipient = useMemo(() => { + if (!request?.recipientAccount) return null + return { + username: request.recipientAccount.user?.username || request.recipientAccount.identifier, + address: request.recipientAddress as Address, + userId: request.recipientAccount.userId, + } + }, [request]) + + // derive total amount and collected + const totalAmount = useMemo(() => { + return request?.tokenAmount ? parseFloat(request.tokenAmount) : 0 + }, [request?.tokenAmount]) + + const totalCollected = useMemo(() => { + return request?.totalCollectedAmount ?? 0 + }, [request?.totalCollectedAmount]) + + // derive contributors from charges + const contributors = useMemo(() => { + if (!request?.charges) return [] + return request.charges + .filter((c) => c.fulfillmentPayment?.status === 'SUCCESSFUL') + .map((c) => ({ + uuid: c.uuid, + username: c.fulfillmentPayment?.payerAccount?.user?.username, + address: c.fulfillmentPayment?.payerAddress ?? undefined, + amount: c.tokenAmount, + createdAt: c.createdAt, + })) + }, [request?.charges]) + + // reset flow + const resetContributePotFlow = useCallback(() => { + setCurrentView('INITIAL') + setAmount('') + setUsdAmount('') + setAttachment({}) + setCharge(null) + setPayment(null) + setTxHash(null) + setError({ showError: false, errorMessage: '' }) + setIsLoading(false) + setIsSuccess(false) + setIsExternalWalletPayment(false) + }, []) + + const value = useMemo( + () => ({ + currentView, + setCurrentView, + request, + setRequest, + recipient, + amount, + setAmount, + usdAmount, + setUsdAmount, + attachment, + setAttachment, + charge, + setCharge, + payment, + setPayment, + txHash, + setTxHash, + error, + setError, + isLoading, + setIsLoading, + isSuccess, + setIsSuccess, + totalAmount, + totalCollected, + contributors, + resetContributePotFlow, + isExternalWalletPayment, + setIsExternalWalletPayment, + }), + [ + currentView, + request, + recipient, + amount, + usdAmount, + attachment, + charge, + payment, + txHash, + error, + isLoading, + isSuccess, + totalAmount, + totalCollected, + contributors, + resetContributePotFlow, + isExternalWalletPayment, + setIsExternalWalletPayment, + ] + ) + + return {children} +} + +export function useContributePotFlowContext() { + const context = useContext(ContributePotFlowContext) + if (!context) { + throw new Error('useContributePotFlowContext must be used within ContributePotFlowProvider') + } + return context +} + +// re-export types for convenience +export type { ContributePotAttachment, ContributePotError } diff --git a/src/features/payments/flows/contribute-pot/ContributePotPage.tsx b/src/features/payments/flows/contribute-pot/ContributePotPage.tsx new file mode 100644 index 000000000..40d60a223 --- /dev/null +++ b/src/features/payments/flows/contribute-pot/ContributePotPage.tsx @@ -0,0 +1,46 @@ +'use client' + +/** + * main entry point for contribute pot flow + * + * wraps content with context provider and renders the correct view: + * - INITIAL: amount input with slider + * - STATUS: success view after payment + * + * receives pre-fetched request data from wrapper + */ + +import { ContributePotFlowProvider, useContributePotFlowContext } from './ContributePotFlowContext' +import { ContributePotInputView } from './views/ContributePotInputView' +import { ContributePotSuccessView } from './views/ContributePotSuccessView' +import { type TRequestResponse } from '@/services/services.types' +import ExternalWalletPaymentView from './views/ExternalWalletPaymentView' + +// internal component that switches views +function ContributePotFlowContent() { + const { currentView } = useContributePotFlowContext() + + switch (currentView) { + case 'STATUS': + return + case 'EXTERNAL_WALLET': + return + case 'INITIAL': + default: + return + } +} + +// props for the page +interface ContributePotPageProps { + request: TRequestResponse +} + +// exported page component with provider +export function ContributePotPage({ request }: ContributePotPageProps) { + return ( + + + + ) +} diff --git a/src/features/payments/flows/contribute-pot/ContributePotPageWrapper.tsx b/src/features/payments/flows/contribute-pot/ContributePotPageWrapper.tsx new file mode 100644 index 000000000..9f7c22bc5 --- /dev/null +++ b/src/features/payments/flows/contribute-pot/ContributePotPageWrapper.tsx @@ -0,0 +1,79 @@ +'use client' + +/** + * wrapper component for ContributePotPage + * + * handles async request fetching before rendering the actual flow. + * shows loading/error states while fetching. + * + * used by: /[...recipient]?id=xyz route when id param is a request pot uuid + */ + +import { ContributePotPage } from './ContributePotPage' +import { requestsApi } from '@/services/requests' +import PeanutLoading from '@/components/Global/PeanutLoading' +import ErrorAlert from '@/components/Global/ErrorAlert' +import NavHeader from '@/components/Global/NavHeader' +import { useRouter } from 'next/navigation' +import { useEffect, useState } from 'react' +import { type TRequestResponse } from '@/services/services.types' + +interface ContributePotPageWrapperProps { + requestId: string +} + +export function ContributePotPageWrapper({ requestId }: ContributePotPageWrapperProps) { + const router = useRouter() + const [request, setRequest] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + // fetch request details + useEffect(() => { + if (!requestId) { + setError('no request id provided') + setIsLoading(false) + return + } + + setIsLoading(true) + setError(null) + + requestsApi + .get(requestId) + .then((data) => { + setRequest(data) + }) + .catch((err) => { + console.error('failed to fetch request:', err) + setError('failed to load request. it may not exist or has been deleted.') + }) + .finally(() => { + setIsLoading(false) + }) + }, [requestId]) + + // loading state + if (isLoading) { + return ( +
+ router.back()} /> +
+ +
+
+ ) + } + + // error state + if (error || !request) { + return ( +
+ router.back()} /> + +
+ ) + } + + return +} diff --git a/src/features/payments/flows/contribute-pot/components/ContributorsDrawer.tsx b/src/features/payments/flows/contribute-pot/components/ContributorsDrawer.tsx new file mode 100644 index 000000000..6eb39894e --- /dev/null +++ b/src/features/payments/flows/contribute-pot/components/ContributorsDrawer.tsx @@ -0,0 +1,70 @@ +'use client' + +/** + * drawer component to show all contributors for a request pot + * + * displays a scrollable list of people who have contributed with: + * - their username or address + * - amount contributed + * + * hidden when there are no contributors yet + */ + +import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/Global/Drawer' +import ContributorCard, { type Contributor } from '@/components/Global/Contributors/ContributorCard' +import { getCardPosition } from '@/components/Global/Card' +import { Button } from '@/components/0_Bruddle/Button' +import { type PotContributor } from '../ContributePotFlowContext' +import { useMemo } from 'react' +import Groups2OutlinedIcon from '@mui/icons-material/Groups2Outlined' + +interface ContributorsDrawerProps { + contributors: PotContributor[] +} + +export function ContributorsDrawer({ contributors }: ContributorsDrawerProps) { + // map to ContributorCard format + const contributorCards = useMemo(() => { + return contributors.map((c) => ({ + uuid: c.uuid, + payments: [], + amount: c.amount, + username: c.username || c.address, + fulfillmentPayment: null, + isUserVerified: false, + isPeanutUser: !!c.username, + })) + }, [contributors]) + + if (contributors.length === 0) { + return null + } + + return ( + + + + + + + Contributors ({contributors.length}) + +
+ {contributorCards.map((contributor, index) => ( + + ))} +
+
+
+ ) +} diff --git a/src/features/payments/flows/contribute-pot/components/RequestPotActionList.tsx b/src/features/payments/flows/contribute-pot/components/RequestPotActionList.tsx new file mode 100644 index 000000000..40baea704 --- /dev/null +++ b/src/features/payments/flows/contribute-pot/components/RequestPotActionList.tsx @@ -0,0 +1,249 @@ +'use client' + +/** + * payment options for request pot flow + * + * shows payment methods for contributing to a request pot: + * - pay with peanut (primary, uses wallet balance) + * - bank/mercadopago/pix (redirects to add-money) + * + * includes smart "use your peanut balance" modal - if user has + * enough balance but clicks on bank, suggests using peanut instead + * + * validates minimum amounts for bank transfers + */ + +import { useMemo, useState } from 'react' +import { useRouter } from 'next/navigation' +import Divider from '@/components/0_Bruddle/Divider' +import StatusBadge from '@/components/Global/Badges/StatusBadge' +import IconStack from '@/components/Global/IconStack' +import Loading from '@/components/Global/Loading' +import ActionModal from '@/components/Global/ActionModal' +import { ActionListCard } from '@/components/ActionListCard' +import { useAuth } from '@/context/authContext' +import { useWallet } from '@/hooks/wallet/useWallet' +import { useGeoFilteredPaymentOptions } from '@/hooks/useGeoFilteredPaymentOptions' +import useKycStatus from '@/hooks/useKycStatus' +import { BankRequestType, useDetermineBankRequestType } from '@/hooks/useDetermineBankRequestType' +import { ACTION_METHODS, type PaymentMethod } from '@/constants/actionlist.consts' +import { MIN_BANK_TRANSFER_AMOUNT, validateMinimumAmount } from '@/constants/payment.consts' +import { saveRedirectUrl, saveToLocalStorage } from '@/utils/general.utils' +import SendWithPeanutCta from '@/features/payments/shared/components/SendWithPeanutCta' + +interface RequestPotActionListProps { + isAmountEntered: boolean + usdAmount: string + recipientUserId?: string + onPayWithPeanut: () => void + isPaymentLoading?: boolean + onPayWithExternalWallet: () => void +} + +export function RequestPotActionList({ + isAmountEntered, + usdAmount, + recipientUserId, + onPayWithPeanut, + isPaymentLoading = false, + onPayWithExternalWallet, +}: RequestPotActionListProps) { + const router = useRouter() + const { user } = useAuth() + const { hasSufficientBalance, isFetchingBalance } = useWallet() + const { isUserMantecaKycApproved } = useKycStatus() + const { requestType } = useDetermineBankRequestType(recipientUserId ?? '') + + const [showMinAmountError, setShowMinAmountError] = useState(false) + const [showUsePeanutBalanceModal, setShowUsePeanutBalanceModal] = useState(false) + const [isUsePeanutBalanceModalShown, setIsUsePeanutBalanceModalShown] = useState(false) + const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(null) + + const isLoggedIn = !!user?.user?.userId + + // check if verification is required for bank + const requiresVerification = useMemo(() => { + return requestType === BankRequestType.GuestKycNeeded || requestType === BankRequestType.PayerKycNeeded + }, [requestType]) + + // check if user has enough peanut balance for the entered amount + // only show insufficient balance after balance has loaded to avoid flash + const userHasSufficientPeanutBalance = useMemo(() => { + if (!user || !usdAmount) return false + if (isFetchingBalance) return true // assume sufficient while loading to avoid flash + return hasSufficientBalance(usdAmount) + }, [user, usdAmount, hasSufficientBalance, isFetchingBalance]) + + // filter and sort payment methods + const { filteredMethods: sortedMethods, isLoading: isGeoLoading } = useGeoFilteredPaymentOptions({ + sortUnavailable: true, + isMethodUnavailable: (method) => + method.soon || + (method.id === 'bank' && requiresVerification) || + (['mercadopago', 'pix'].includes(method.id) && !isUserMantecaKycApproved), + methods: ACTION_METHODS, + }) + + // handle clicking on a payment method + const handleMethodClick = (method: PaymentMethod, bypassBalanceModal = false) => { + const requestAmountValue = parseFloat(usdAmount || '0') + + // validate minimum amount for bank/mercadopago/pix against user-entered amount + if ( + ['bank', 'mercadopago', 'pix'].includes(method.id) && + !validateMinimumAmount(requestAmountValue, method.id) + ) { + setShowMinAmountError(true) + return + } + + // if user has sufficient peanut balance and hasn't dismissed the modal, suggest using peanut + if (!bypassBalanceModal && !isUsePeanutBalanceModalShown && userHasSufficientPeanutBalance) { + setSelectedPaymentMethod(method) + setShowUsePeanutBalanceModal(true) + return + } + + if (method.id === 'exchange-or-wallet') { + onPayWithExternalWallet() + return + } + + // redirect to add-money flow for bank/mercadopago/pix + switch (method.id) { + case 'bank': + case 'mercadopago': + case 'pix': + if (isLoggedIn) { + // save current url so back button works properly + saveRedirectUrl() + // flag that we're coming from request fulfillment + saveToLocalStorage('fromRequestFulfillment', 'true') + router.push('/add-money') + } else { + const redirectUri = encodeURIComponent('/add-money') + router.push(`/setup?redirect_uri=${redirectUri}`) + } + break + } + } + + // handle continue with peanut (for non-logged in users) + const handleContinueWithPeanut = () => { + if (!isLoggedIn) { + saveRedirectUrl() + router.push('/setup') + } else { + onPayWithPeanut() + } + } + + if (isGeoLoading) { + return ( +
+ +
+ ) + } + + return ( +
+ {/* pay with peanut button */} + + + + + {/* payment methods */} +
+ {sortedMethods.map((method) => { + let methodRequiresVerification = method.id === 'bank' && requiresVerification + if (!isUserMantecaKycApproved && ['mercadopago', 'pix'].includes(method.id)) { + methodRequiresVerification = true + } + + return ( + + {method.title} + {(method.soon || methodRequiresVerification) && ( + + )} +
+ } + onClick={() => handleMethodClick(method)} + isDisabled={method.soon || !isAmountEntered} + rightContent={} + /> + ) + })} +
+ + {/* minimum amount error modal */} + setShowMinAmountError(false)} + title="Minimum Amount" + description={`The minimum amount for this payment method is $${MIN_BANK_TRANSFER_AMOUNT}. Please enter a higher amount or try a different method.`} + icon="alert" + ctas={[{ text: 'Close', shadowSize: '4', onClick: () => setShowMinAmountError(false) }]} + iconContainerClassName="bg-yellow-400" + preventClose={false} + modalPanelClassName="max-w-md mx-8" + /> + + {/* use peanut balance modal - only shown when user has enough balance */} + { + setShowUsePeanutBalanceModal(false) + setIsUsePeanutBalanceModalShown(true) + setSelectedPaymentMethod(null) + }} + title="Use your Peanut balance instead" + description="You already have enough funds in your Peanut account. Using this method is instant and avoids delays." + icon="user-plus" + ctas={[ + { + text: 'Pay with Peanut', + shadowSize: '4', + onClick: () => { + setShowUsePeanutBalanceModal(false) + setIsUsePeanutBalanceModalShown(true) + setSelectedPaymentMethod(null) + onPayWithPeanut() + }, + }, + { + text: 'Continue', + shadowSize: '4', + variant: 'stroke', + onClick: () => { + setShowUsePeanutBalanceModal(false) + setIsUsePeanutBalanceModalShown(true) + if (selectedPaymentMethod) { + handleMethodClick(selectedPaymentMethod, true) + } + setSelectedPaymentMethod(null) + }, + }, + ]} + iconContainerClassName="bg-primary-1" + preventClose={false} + modalPanelClassName="max-w-md mx-8" + /> +
+ ) +} diff --git a/src/features/payments/flows/contribute-pot/useContributePotFlow.ts b/src/features/payments/flows/contribute-pot/useContributePotFlow.ts new file mode 100644 index 000000000..997a98759 --- /dev/null +++ b/src/features/payments/flows/contribute-pot/useContributePotFlow.ts @@ -0,0 +1,282 @@ +'use client' + +/** + * hook for contribute pot flow + * + * handles the full payment lifecycle for request pot contributions: + * 1. validates amount and checks balance + * 2. creates a charge in backend + * 3. sends payment via peanut wallet + * 4. records the payment to backend + * + * also provides smart defaults for the contribution slider + * based on existing contributions (e.g. suggests 1/3 for split bills) + */ + +import { useCallback, useMemo } from 'react' +import { type Address, type Hash } from 'viem' +import { useContributePotFlowContext } from './ContributePotFlowContext' +import { useChargeManager } from '@/features/payments/shared/hooks/useChargeManager' +import { usePaymentRecorder } from '@/features/payments/shared/hooks/usePaymentRecorder' +import { useWallet } from '@/hooks/wallet/useWallet' +import { useAuth } from '@/context/authContext' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN, PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' +import { ErrorHandler } from '@/utils/sdkErrorHandler.utils' + +export function useContributePotFlow() { + const { + amount, + setAmount, + usdAmount, + setUsdAmount, + currentView, + setCurrentView, + request, + recipient, + attachment, + setAttachment, + charge, + setCharge, + payment, + setPayment, + txHash, + setTxHash, + error, + setError, + isLoading, + setIsLoading, + isSuccess, + setIsSuccess, + totalAmount, + totalCollected, + contributors, + resetContributePotFlow, + isExternalWalletPayment, + setIsExternalWalletPayment, + } = useContributePotFlowContext() + + const { user } = useAuth() + const { createCharge, isCreating: isCreatingCharge } = useChargeManager() + const { recordPayment, isRecording } = usePaymentRecorder() + const { + isConnected, + address: walletAddress, + sendMoney, + formattedBalance, + hasSufficientBalance, + isFetchingBalance, + } = useWallet() + + const isLoggedIn = !!user?.user?.userId + + // set amount (for peanut wallet, amount is always in usd) + const handleSetAmount = useCallback( + (value: string) => { + setAmount(value) + setUsdAmount(value) + }, + [setAmount, setUsdAmount] + ) + + // clear error + const clearError = useCallback(() => { + setError({ showError: false, errorMessage: '' }) + }, [setError]) + + // check if can proceed + const canProceed = useMemo(() => { + if (!amount || !recipient || !request) return false + const amountNum = parseFloat(amount) + if (isNaN(amountNum) || amountNum <= 0) return false + return true + }, [amount, recipient, request]) + + // check if has sufficient balance for current amount + const hasEnoughBalance = useMemo(() => { + if (!amount) return false + return hasSufficientBalance(amount) + }, [amount, hasSufficientBalance]) + + // check if should show insufficient balance error + // only show after balance has loaded to avoid flash of error on initial render + const isInsufficientBalance = useMemo(() => { + return ( + isLoggedIn && + !!amount && + !hasEnoughBalance && + !isLoading && + !isCreatingCharge && + !isRecording && + !isFetchingBalance + ) + }, [isLoggedIn, amount, hasEnoughBalance, isLoading, isCreatingCharge, isRecording, isFetchingBalance]) + + // calculate default slider value and suggested amount + const sliderDefaults = useMemo(() => { + if (totalAmount <= 0) return { percentage: 0, suggestedAmount: 0 } + + // no contributions yet - suggest 100% (full pot) + if (contributors.length === 0) { + return { percentage: 100, suggestedAmount: totalAmount } + } + + // calculate based on existing contributions + const contributionAmounts = contributors.map((c) => parseFloat(c.amount)).filter((a) => !isNaN(a) && a > 0) + + if (contributionAmounts.length === 0) return { percentage: 0, suggestedAmount: 0 } + + // check if this is an equal-split pattern + const collectedPercentage = (totalCollected / totalAmount) * 100 + const isOneThirdCollected = Math.abs(collectedPercentage - 100 / 3) < 2 + const isTwoThirdsCollected = Math.abs(collectedPercentage - 200 / 3) < 2 + + if (isOneThirdCollected || isTwoThirdsCollected) { + const exactThird = 100 / 3 + return { percentage: exactThird, suggestedAmount: totalAmount * (exactThird / 100) } + } + + // suggest median contribution + const sortedAmounts = [...contributionAmounts].sort((a, b) => a - b) + const midIndex = Math.floor(sortedAmounts.length / 2) + const suggestedAmount = + sortedAmounts.length % 2 === 0 + ? (sortedAmounts[midIndex - 1] + sortedAmounts[midIndex]) / 2 + : sortedAmounts[midIndex] + + const percentage = Math.min((suggestedAmount / totalAmount) * 100, 100) + return { percentage, suggestedAmount } + }, [totalAmount, totalCollected, contributors]) + + // execute the contribution + const executeContribution = useCallback( + async ( + shouldReturnAfterCreatingCharge: boolean = false, + bypassLoginCheck: boolean = false + ): Promise<{ success: boolean }> => { + if (!recipient || !amount || !request) { + setError({ showError: true, errorMessage: 'missing required data' }) + return { success: false } + } + + if (!bypassLoginCheck && !walletAddress) { + setError({ showError: true, errorMessage: 'Please login to continue' }) + return { success: false } + } + + setIsLoading(true) + clearError() + + try { + // step 1: create charge for this contribution + const chargeResult = await createCharge({ + tokenAmount: amount, + tokenAddress: PEANUT_WALLET_TOKEN as Address, + chainId: PEANUT_WALLET_CHAIN.id.toString(), + tokenSymbol: 'USDC', + tokenDecimals: PEANUT_WALLET_TOKEN_DECIMALS, + recipientAddress: recipient.address, + transactionType: 'REQUEST', + requestId: request.uuid, + reference: attachment.message, + attachment: attachment.file, + currencyAmount: usdAmount, + currencyCode: 'USD', + }) + + setCharge(chargeResult) + + // Return early after creating charge if requested, used in external wallet flow. + if (shouldReturnAfterCreatingCharge) { + setIsLoading(false) + return { success: true } + } + + // step 2: send money via peanut wallet + const txResult = await sendMoney(recipient.address, amount) + const hash = (txResult.receipt?.transactionHash ?? txResult.userOpHash) as Hash + + setTxHash(hash) + + // step 3: record payment to backend + const paymentResult = await recordPayment({ + chargeId: chargeResult.uuid, + chainId: PEANUT_WALLET_CHAIN.id.toString(), + txHash: hash, + tokenAddress: PEANUT_WALLET_TOKEN as Address, + payerAddress: walletAddress as Address, + }) + + setPayment(paymentResult) + setIsSuccess(true) + setCurrentView('STATUS') + setIsLoading(false) + return { success: true } + } catch (err) { + const errorMessage = ErrorHandler(err) + setError({ showError: true, errorMessage }) + setIsLoading(false) + return { success: false } + } + }, + [ + recipient, + amount, + usdAmount, + attachment, + walletAddress, + request, + createCharge, + sendMoney, + recordPayment, + setCharge, + setTxHash, + setPayment, + setIsSuccess, + setCurrentView, + setError, + setIsLoading, + clearError, + ] + ) + + return { + // state + amount, + usdAmount, + currentView, + request, + recipient, + attachment, + charge, + payment, + txHash, + error, + isLoading: isLoading || isCreatingCharge || isRecording, + isSuccess, + isExternalWalletPayment, + + // derived data + totalAmount, + totalCollected, + contributors, + sliderDefaults, + + // computed + canProceed, + hasSufficientBalance: hasEnoughBalance, + isInsufficientBalance, + isLoggedIn, + isConnected, + walletAddress, + formattedBalance, + + // actions + setAmount: handleSetAmount, + setAttachment, + clearError, + executeContribution, + resetContributePotFlow, + setCurrentView, + setIsExternalWalletPayment, + } +} diff --git a/src/features/payments/flows/contribute-pot/views/ContributePotInputView.tsx b/src/features/payments/flows/contribute-pot/views/ContributePotInputView.tsx new file mode 100644 index 000000000..1c58697e8 --- /dev/null +++ b/src/features/payments/flows/contribute-pot/views/ContributePotInputView.tsx @@ -0,0 +1,135 @@ +'use client' + +/** + * input view for contribute pot flow + * + * displays: + * - recipient card with pot progress (amount collected / total) + * - amount input with slider (defaults to smart suggestion) + * - payment method options + * - contributors drawer (see who else paid) + * + * executes payment directly on submit + */ + +import NavHeader from '@/components/Global/NavHeader' +import AmountInput from '@/components/Global/AmountInput' +import UserCard from '@/components/User/UserCard' +import ErrorAlert from '@/components/Global/ErrorAlert' +import SupportCTA from '@/components/Global/SupportCTA' +import { useContributePotFlow } from '../useContributePotFlow' +import { useRouter } from 'next/navigation' +import { useAuth } from '@/context/authContext' +import { RequestPotActionList } from '../components/RequestPotActionList' + +export function ContributePotInputView() { + const router = useRouter() + const { isFetchingUser } = useAuth() + const { + amount, + request, + recipient, + error, + formattedBalance, + canProceed, + hasSufficientBalance, + isInsufficientBalance, + isLoggedIn, + isLoading, + totalAmount, + totalCollected, + contributors, + sliderDefaults, + setAmount, + executeContribution, + setCurrentView, + } = useContributePotFlow() + + // handle submit - directly execute contribution + const handlePayWithPeanut = () => { + if (canProceed && hasSufficientBalance && !isLoading) { + executeContribution() + } + } + + // handle back navigation + const handleGoBack = () => { + if (window.history.length > 1) { + router.back() + } else { + router.push('/') + } + } + + // handle External Wallet click + const handleOpenExternalWalletFlow = async () => { + if (canProceed && !isLoading) { + const res = await executeContribution(true, true) // return after creating charge + // Proceed only if charge is created successfully + if (res && res.success) { + setCurrentView('EXTERNAL_WALLET') + } + } + } + + // determine button state + const isAmountEntered = !!amount && parseFloat(amount) > 0 + + return ( +
+ + +
+ {/* recipient card with pot info */} + {recipient && ( + + )} + + {/* amount input with slider */} + 0} + maxAmount={totalAmount} + amountCollected={totalCollected} + defaultSliderValue={sliderDefaults.percentage} + defaultSliderSuggestedAmount={sliderDefaults.suggestedAmount} + /> + + {/* error display */} + {isInsufficientBalance && ( + + )} + {error.showError && } + + {/* payment options */} + +
+ + {/* support cta */} + {!isFetchingUser && } +
+ ) +} diff --git a/src/features/payments/flows/contribute-pot/views/ContributePotSuccessView.tsx b/src/features/payments/flows/contribute-pot/views/ContributePotSuccessView.tsx new file mode 100644 index 000000000..0ac78fa9f --- /dev/null +++ b/src/features/payments/flows/contribute-pot/views/ContributePotSuccessView.tsx @@ -0,0 +1,45 @@ +'use client' + +/** + * success view for contribute pot flow + * + * thin wrapper around PaymentSuccessView that: + * - pulls data from contribute pot flow context + * - calculates points earned for the contribution + * - provides reset callback on completion + */ + +import PaymentSuccessView from '@/features/payments/shared/components/PaymentSuccessView' +import { useContributePotFlow } from '../useContributePotFlow' +import { usePointsCalculation } from '@/hooks/usePointsCalculation' +import { PointsAction } from '@/services/services.types' + +export function ContributePotSuccessView() { + const { usdAmount, recipient, attachment, charge, payment, resetContributePotFlow, isExternalWalletPayment } = + useContributePotFlow() + + // calculate points for the contribution + const { pointsData } = usePointsCalculation( + PointsAction.P2P_REQUEST_PAYMENT, + usdAmount, + !!payment || isExternalWalletPayment, // For external wallet payments, we dont't have payment info on the FE, its handled by webooks on BE + payment?.uuid, + recipient?.userId + ) + + return ( + + ) +} diff --git a/src/features/payments/flows/contribute-pot/views/ExternalWalletPaymentView.tsx b/src/features/payments/flows/contribute-pot/views/ExternalWalletPaymentView.tsx new file mode 100644 index 000000000..c87abf24a --- /dev/null +++ b/src/features/payments/flows/contribute-pot/views/ExternalWalletPaymentView.tsx @@ -0,0 +1,58 @@ +'use client' + +import RhinoDepositView from '@/components/AddMoney/views/RhinoDeposit.view' +import { useContributePotFlow } from '../useContributePotFlow' +import { useCallback, useState } from 'react' +import type { RhinoChainType } from '@/services/services.types' +import { useQuery } from '@tanstack/react-query' +import { rhinoApi } from '@/services/rhino' +import { useWallet } from '@/hooks/wallet/useWallet' + +const ExternalWalletPaymentView = () => { + const { charge, setCurrentView, setIsExternalWalletPayment, amount, request } = useContributePotFlow() + const [chainType, setChainType] = useState('EVM') + const { address: peanutWalletAddress } = useWallet() + + const { data: depositAddressData, isLoading } = useQuery({ + queryKey: ['rhino-deposit-address', charge?.uuid, chainType], + queryFn: () => { + if (!charge?.requestLink.uuid) { + throw new Error('Request ID is required') + } + if (!charge.uuid) { + throw new Error('Charge ID is required') + } + return rhinoApi.createRequestFulfilmentAddress(chainType, charge?.uuid as string, peanutWalletAddress) + }, + enabled: !!charge, + staleTime: 1000 * 60 * 60 * 24, // 24 hours + }) + + const onSuccess = useCallback((_: number) => { + setIsExternalWalletPayment(true) + setCurrentView('STATUS') + }, []) + + return ( +
+ setCurrentView('INITIAL')} + identifier={ + request?.recipientAccount.type === 'peanut-wallet' + ? request?.recipientAccount.user.username + : request?.recipientAccount.identifier + } + amount={Number(amount)} + /> +
+ ) +} + +export default ExternalWalletPaymentView diff --git a/src/features/payments/flows/direct-send/DirectSendFlowContext.tsx b/src/features/payments/flows/direct-send/DirectSendFlowContext.tsx new file mode 100644 index 000000000..e2e03d613 --- /dev/null +++ b/src/features/payments/flows/direct-send/DirectSendFlowContext.tsx @@ -0,0 +1,155 @@ +'use client' + +/** + * context for send flow state management + * + * direct send flow for paying another peanut user by username. + * payments are always usdc on arbitrum (peanut wallet). + * + * route: /send/username + * + */ + +import React, { createContext, useContext, useMemo, useState, useCallback, type ReactNode } from 'react' +import { type Address, type Hash } from 'viem' +import { type TRequestChargeResponse, type PaymentCreationResponse } from '@/services/services.types' + +// view states +export type DirectSendFlowView = 'INITIAL' | 'STATUS' + +// recipient info +export interface DirectSendRecipient { + username: string + address: Address + userId?: string + fullName?: string +} + +// attachment options +export interface DirectSendAttachment { + message?: string + file?: File + fileUrl?: string +} + +// error state for input view +export interface DirectSendFlowErrorState { + showError: boolean + errorMessage: string +} + +// context type +interface DirectSendFlowContextType { + // state + amount: string + setAmount: (amount: string) => void + usdAmount: string + setUsdAmount: (amount: string) => void + currentView: DirectSendFlowView + setCurrentView: (view: DirectSendFlowView) => void + recipient: DirectSendRecipient | null + setRecipient: (recipient: DirectSendRecipient | null) => void + attachment: DirectSendAttachment + setAttachment: (attachment: DirectSendAttachment) => void + charge: TRequestChargeResponse | null + setCharge: (charge: TRequestChargeResponse | null) => void + payment: PaymentCreationResponse | null + setPayment: (payment: PaymentCreationResponse | null) => void + txHash: Hash | null + setTxHash: (hash: Hash | null) => void + error: DirectSendFlowErrorState + setError: (error: DirectSendFlowErrorState) => void + isLoading: boolean + setIsLoading: (loading: boolean) => void + isSuccess: boolean + setIsSuccess: (success: boolean) => void + + // actions + resetSendFlow: () => void +} + +const DirectSendFlowContext = createContext(undefined) + +interface SendFlowProviderProps { + children: ReactNode + initialRecipient?: DirectSendRecipient +} + +export const DirectSendFlowProvider: React.FC = ({ children, initialRecipient }) => { + const [amount, setAmount] = useState('') + const [usdAmount, setUsdAmount] = useState('') + const [currentView, setCurrentView] = useState('INITIAL') + const [recipient, setRecipient] = useState(initialRecipient ?? null) + const [attachment, setAttachment] = useState({}) + const [charge, setCharge] = useState(null) + const [payment, setPayment] = useState(null) + const [txHash, setTxHash] = useState(null) + const [error, setError] = useState({ showError: false, errorMessage: '' }) + const [isLoading, setIsLoading] = useState(false) + const [isSuccess, setIsSuccess] = useState(false) + + const resetSendFlow = useCallback(() => { + setAmount('') + setUsdAmount('') + setCurrentView('INITIAL') + setAttachment({}) + setCharge(null) + setPayment(null) + setTxHash(null) + setError({ showError: false, errorMessage: '' }) + setIsLoading(false) + setIsSuccess(false) + }, []) + + const value = useMemo( + () => ({ + amount, + setAmount, + usdAmount, + setUsdAmount, + currentView, + setCurrentView, + recipient, + setRecipient, + attachment, + setAttachment, + charge, + setCharge, + payment, + setPayment, + txHash, + setTxHash, + error, + setError, + isLoading, + setIsLoading, + isSuccess, + setIsSuccess, + resetSendFlow, + }), + [ + amount, + usdAmount, + currentView, + recipient, + attachment, + charge, + payment, + txHash, + error, + isLoading, + isSuccess, + resetSendFlow, + ] + ) + + return {children} +} + +export const useDirectSendFlowContext = (): DirectSendFlowContextType => { + const context = useContext(DirectSendFlowContext) + if (context === undefined) { + throw new Error('useDirectSendFlowContext must be used within DirectSendFlowProvider') + } + return context +} diff --git a/src/features/payments/flows/direct-send/DirectSendPage.tsx b/src/features/payments/flows/direct-send/DirectSendPage.tsx new file mode 100644 index 000000000..2c37055f1 --- /dev/null +++ b/src/features/payments/flows/direct-send/DirectSendPage.tsx @@ -0,0 +1,42 @@ +'use client' + +/** + * main entry point for direct send flow + * + * wraps content with context provider and renders the correct view: + * - INITIAL: amount input with optional message + * - STATUS: success view after payment + * + * receives pre-resolved recipient data from wrapper + */ + +import { DirectSendFlowProvider, useDirectSendFlowContext, type DirectSendRecipient } from './DirectSendFlowContext' +import { SendInputView } from './views/SendInputView' +import { SendSuccessView } from './views/SendSuccessView' + +// internal component that switches views +function SendFlowContent() { + const { currentView } = useDirectSendFlowContext() + + switch (currentView) { + case 'STATUS': + return + case 'INITIAL': + default: + return + } +} + +// props for the page +interface DirectSendPageProps { + recipient: DirectSendRecipient +} + +// exported page component with provider +export function DirectSendPage({ recipient }: DirectSendPageProps) { + return ( + + + + ) +} diff --git a/src/features/payments/flows/direct-send/DirectSendPageWrapper.tsx b/src/features/payments/flows/direct-send/DirectSendPageWrapper.tsx new file mode 100644 index 000000000..06136fc82 --- /dev/null +++ b/src/features/payments/flows/direct-send/DirectSendPageWrapper.tsx @@ -0,0 +1,72 @@ +'use client' + +/** + * wrapper component for DirectSendPage + * + * handles async username resolution before rendering the actual flow. + * finds the user's peanut wallet address from their username. + * + * shows loading/error states while resolving + * + * used by: /send/[...username] route + */ + +import { useUserByUsername } from '@/hooks/useUserByUsername' +import { AccountType } from '@/interfaces' +import PeanutLoading from '@/components/Global/PeanutLoading' +import ErrorAlert from '@/components/Global/ErrorAlert' +import NavHeader from '@/components/Global/NavHeader' +import { useRouter } from 'next/navigation' +import { useMemo } from 'react' +import { type Address } from 'viem' +import type { DirectSendRecipient } from './DirectSendFlowContext' +import { DirectSendPage } from './DirectSendPage' + +interface DirectSendPageWrapperProps { + username: string +} + +export function DirectSendPageWrapper({ username }: DirectSendPageWrapperProps) { + const router = useRouter() + const { user, isLoading, error } = useUserByUsername(username) + + // resolve user to recipient + const recipient = useMemo(() => { + if (!user) return null + + // find peanut wallet address + const walletAccount = user.accounts.find((acc) => acc.type === AccountType.PEANUT_WALLET) + if (!walletAccount) return null + + return { + username: user.username, + address: walletAccount.identifier as Address, + userId: user.userId, + fullName: user.fullName, + } + }, [user]) + + // loading state + if (isLoading) { + return ( +
+ router.back()} /> +
+ +
+
+ ) + } + + // error state + if (error || !recipient) { + return ( +
+ router.back()} /> + +
+ ) + } + + return +} diff --git a/src/features/payments/flows/direct-send/useDirectSendFlow.ts b/src/features/payments/flows/direct-send/useDirectSendFlow.ts new file mode 100644 index 000000000..81cf02930 --- /dev/null +++ b/src/features/payments/flows/direct-send/useDirectSendFlow.ts @@ -0,0 +1,193 @@ +'use client' + +/** + * hook for direct send flow + * + * handles the full payment lifecycle for direct sends to peanut users: + * 1. validates amount and checks balance + * 2. creates a charge in backend + * 3. sends usdc via peanut wallet + * 4. records the payment to backend + * + * note: no cross-chain, always usdc on arbitrum + */ + +import { useCallback, useMemo } from 'react' +import { type Address, type Hash } from 'viem' +import { useDirectSendFlowContext } from './DirectSendFlowContext' +import { useChargeManager } from '@/features/payments/shared/hooks/useChargeManager' +import { usePaymentRecorder } from '@/features/payments/shared/hooks/usePaymentRecorder' +import { useWallet } from '@/hooks/wallet/useWallet' +import { useAuth } from '@/context/authContext' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN, PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' +import { ErrorHandler } from '@/utils/sdkErrorHandler.utils' + +export function useDirectSendFlow() { + const { + amount, + setAmount, + usdAmount, + setUsdAmount, + currentView, + setCurrentView, + recipient, + attachment, + setAttachment, + charge, + setCharge, + payment, + setPayment, + txHash, + setTxHash, + error, + setError, + isLoading, + setIsLoading, + isSuccess, + setIsSuccess, + resetSendFlow, + } = useDirectSendFlowContext() + + const { user } = useAuth() + const { createCharge, isCreating: isCreatingCharge } = useChargeManager() + const { recordPayment, isRecording } = usePaymentRecorder() + const { isConnected, address: walletAddress, sendMoney, formattedBalance, hasSufficientBalance } = useWallet() + + const isLoggedIn = !!user?.user?.userId + + // set amount (for peanut wallet, amount is always in usd) + const handleSetAmount = useCallback( + (value: string) => { + setAmount(value) + setUsdAmount(value) + }, + [setAmount, setUsdAmount] + ) + + // clear error + const clearError = useCallback(() => { + setError({ showError: false, errorMessage: '' }) + }, [setError]) + + // check if can proceed + const canProceed = useMemo(() => { + if (!amount || !recipient) return false + const amountNum = parseFloat(amount) + if (isNaN(amountNum) || amountNum <= 0) return false + return true + }, [amount, recipient]) + + // check if has sufficient balance for current amount + const hasEnoughBalance = useMemo(() => { + if (!amount) return false + return hasSufficientBalance(amount) + }, [amount, hasSufficientBalance]) + + // check if should show insufficient balance error + const isInsufficientBalance = useMemo(() => { + return isLoggedIn && !!amount && !hasEnoughBalance && !isLoading && !isCreatingCharge && !isRecording + }, [isLoggedIn, amount, hasEnoughBalance, isLoading, isCreatingCharge, isRecording]) + + // execute the payment (called from input view) + const executePayment = useCallback(async () => { + if (!recipient || !amount || !walletAddress) { + setError({ showError: true, errorMessage: 'missing required data' }) + return + } + + setIsLoading(true) + clearError() + + try { + // step 1: create charge + const chargeResult = await createCharge({ + tokenAmount: amount, + tokenAddress: PEANUT_WALLET_TOKEN as Address, + chainId: PEANUT_WALLET_CHAIN.id.toString(), + tokenSymbol: 'USDC', + tokenDecimals: PEANUT_WALLET_TOKEN_DECIMALS, + recipientAddress: recipient.address, + transactionType: 'DIRECT_SEND', + reference: attachment.message, + attachment: attachment.file, + currencyAmount: usdAmount, + currencyCode: 'USD', + }) + + setCharge(chargeResult) + + // step 2: send money via peanut wallet + const txResult = await sendMoney(recipient.address, amount) + const hash = (txResult.receipt?.transactionHash ?? txResult.userOpHash) as Hash + + setTxHash(hash) + + // step 3: record payment to backend + const paymentResult = await recordPayment({ + chargeId: chargeResult.uuid, + chainId: PEANUT_WALLET_CHAIN.id.toString(), + txHash: hash, + tokenAddress: PEANUT_WALLET_TOKEN as Address, + payerAddress: walletAddress as Address, + }) + + setPayment(paymentResult) + setIsSuccess(true) + setCurrentView('STATUS') + } catch (err) { + const errorMessage = ErrorHandler(err) + setError({ showError: true, errorMessage }) + } finally { + setIsLoading(false) + } + }, [ + recipient, + amount, + usdAmount, + attachment, + walletAddress, + createCharge, + sendMoney, + recordPayment, + setCharge, + setTxHash, + setPayment, + setIsSuccess, + setCurrentView, + setError, + setIsLoading, + clearError, + ]) + + return { + // state + amount, + usdAmount, + currentView, + recipient, + attachment, + charge, + payment, + txHash, + error, + isLoading: isLoading || isCreatingCharge || isRecording, + isSuccess, + + // computed + canProceed, + hasSufficientBalance: hasEnoughBalance, + isInsufficientBalance, + isLoggedIn, + isConnected, + walletAddress, + formattedBalance, + + // actions + setAmount: handleSetAmount, + setAttachment, + clearError, + executePayment, + resetSendFlow, + setCurrentView, + } +} diff --git a/src/features/payments/flows/direct-send/views/SendInputView.tsx b/src/features/payments/flows/direct-send/views/SendInputView.tsx new file mode 100644 index 000000000..d46614f89 --- /dev/null +++ b/src/features/payments/flows/direct-send/views/SendInputView.tsx @@ -0,0 +1,132 @@ +'use client' + +/** + * input view for send flow + * + * displays: + * - recipient card (peanut username) + * - amount input + * - optional message/file attachment + * - payment method options + * + * executes payment directly on submit (no confirm step) + */ + +import NavHeader from '@/components/Global/NavHeader' +import AmountInput from '@/components/Global/AmountInput' +import UserCard from '@/components/User/UserCard' +import FileUploadInput from '@/components/Global/FileUploadInput' +import ErrorAlert from '@/components/Global/ErrorAlert' +import SupportCTA from '@/components/Global/SupportCTA' +import { useDirectSendFlow } from '../useDirectSendFlow' +import { useRouter } from 'next/navigation' +import { useAuth } from '@/context/authContext' +import SendWithPeanutCta from '@/features/payments/shared/components/SendWithPeanutCta' +import { PaymentMethodActionList } from '@/features/payments/shared/components/PaymentMethodActionList' + +export function SendInputView() { + const router = useRouter() + const { isFetchingUser } = useAuth() + const { + amount, + recipient, + attachment, + error, + formattedBalance, + canProceed, + hasSufficientBalance, + isInsufficientBalance, + isLoggedIn, + isLoading, + setAmount, + setAttachment, + executePayment, + } = useDirectSendFlow() + + // handle submit - directly execute payment + const handleSubmit = () => { + if (canProceed && hasSufficientBalance && !isLoading) { + executePayment() + } + } + + // handle back navigation + const handleGoBack = () => { + if (window.history.length > 1) { + router.back() + } else { + router.push('/') + } + } + + // determine button text and state + const isButtonDisabled = !canProceed || (isLoggedIn && !hasSufficientBalance) || isLoading + const isAmountEntered = !!amount && parseFloat(amount) > 0 + + return ( +
+ + +
+ {/* recipient card */} + {recipient && ( + + )} + + {/* amount input */} + + + {/* message input */} + + setAttachment({ + message: opts.message, + file: opts.rawFile, + fileUrl: opts.fileUrl, + }) + } + className="h-11" + /> + + {/* button and error */} +
+ + {isInsufficientBalance && ( + + )} + {error.showError && } +
+ + {/* action list for non-logged in users */} + {!isLoggedIn && !isFetchingUser && } +
+ + {/* support cta for guest users */} + {!isLoggedIn && !isFetchingUser && } +
+ ) +} diff --git a/src/features/payments/flows/direct-send/views/SendSuccessView.tsx b/src/features/payments/flows/direct-send/views/SendSuccessView.tsx new file mode 100644 index 000000000..c12d54b6c --- /dev/null +++ b/src/features/payments/flows/direct-send/views/SendSuccessView.tsx @@ -0,0 +1,44 @@ +'use client' + +/** + * success view for send flow + * + * thin wrapper around PaymentSuccessView that: + * - pulls data from send flow context + * - calculates points earned for the send + * - provides reset callback on completion + */ + +import PaymentSuccessView from '@/features/payments/shared/components/PaymentSuccessView' +import { useDirectSendFlow } from '../useDirectSendFlow' +import { usePointsCalculation } from '@/hooks/usePointsCalculation' +import { PointsAction } from '@/services/services.types' + +export function SendSuccessView() { + const { usdAmount, recipient, attachment, charge, payment, resetSendFlow } = useDirectSendFlow() + + // calculate points for the transaction + const { pointsData } = usePointsCalculation( + PointsAction.P2P_SEND_LINK, + usdAmount, + !!payment, + payment?.uuid, + recipient?.userId + ) + + return ( + + ) +} diff --git a/src/features/payments/flows/semantic-request/SemanticRequestFlowContext.tsx b/src/features/payments/flows/semantic-request/SemanticRequestFlowContext.tsx new file mode 100644 index 000000000..682f18c2e --- /dev/null +++ b/src/features/payments/flows/semantic-request/SemanticRequestFlowContext.tsx @@ -0,0 +1,237 @@ +'use client' + +/** + * context provider for semantic request flow state + * + * handles payments via semantic urls like: + * - /username (peanut user) + * - /0x1234... (address) + * - /vitalik.eth (ens) + * - /username/10/usdc/arbitrum (with amount/token/chain) + * + * supports cross-chain payments - user pays in usdc on arbitrum, + * recipient can receive on different chain/token + * + * note: token/chain selection uses tokenSelectorContext + */ + +import { createContext, useContext, useState, useMemo, useCallback, type ReactNode } from 'react' +import { type Address, type Hash } from 'viem' +import { type TRequestChargeResponse, type PaymentCreationResponse } from '@/services/services.types' +import { type ParsedURL, type RecipientType } from '@/lib/url-parser/types/payment' + +// view states for semantic request flow +export type SemanticRequestFlowView = 'INITIAL' | 'CONFIRM' | 'STATUS' | 'RECEIPT' | 'EXTERNAL_WALLET' + +// recipient info from parsed url +export interface SemanticRequestRecipient { + identifier: string + recipientType: RecipientType + resolvedAddress: Address +} + +// attachment state +interface SemanticRequestAttachment { + message?: string + file?: File + fileUrl?: string +} + +// error state +interface SemanticRequestError { + showError: boolean + errorMessage: string +} + +// context value type +interface SemanticRequestFlowContextValue { + // view state + currentView: SemanticRequestFlowView + setCurrentView: (view: SemanticRequestFlowView) => void + + // parsed url data + parsedUrl: ParsedURL | null + setParsedUrl: (data: ParsedURL | null) => void + + // recipient (from parsed url) + recipient: SemanticRequestRecipient | null + + // charge id from url (for direct confirm view access) + chargeIdFromUrl: string | undefined + + // amount state (can be preset from url or entered) + amount: string + setAmount: (amount: string) => void + usdAmount: string + setUsdAmount: (amount: string) => void + isAmountFromUrl: boolean + isTokenFromUrl: boolean + isChainFromUrl: boolean + + // attachment state + attachment: SemanticRequestAttachment + setAttachment: (attachment: SemanticRequestAttachment) => void + + // charge and payment results + charge: TRequestChargeResponse | null + setCharge: (charge: TRequestChargeResponse | null) => void + payment: PaymentCreationResponse | null + setPayment: (payment: PaymentCreationResponse | null) => void + txHash: Hash | null + setTxHash: (hash: Hash | null) => void + + // ui state + error: SemanticRequestError + setError: (error: SemanticRequestError) => void + isLoading: boolean + setIsLoading: (loading: boolean) => void + isSuccess: boolean + setIsSuccess: (success: boolean) => void + isExternalWalletPayment: boolean + setIsExternalWalletPayment: (isExternalWalletPayment: boolean) => void + + // actions + resetSemanticRequestFlow: () => void +} + +const SemanticRequestFlowContext = createContext(null) + +interface SemanticRequestFlowProviderProps { + children: ReactNode + initialParsedUrl: ParsedURL + initialChargeId?: string +} + +export function SemanticRequestFlowProvider({ + children, + initialParsedUrl, + initialChargeId, +}: SemanticRequestFlowProviderProps) { + // view state - determine initial view based on chargeId and recipient type + // for usernames with chargeId: start at INITIAL (direct request flow) + // for address/ens with chargeId: start at CONFIRM (semantic request payment) + const [currentView, setCurrentView] = useState(() => { + if (!initialChargeId) return 'INITIAL' + const isUsernameRecipient = initialParsedUrl.recipient?.recipientType === 'USERNAME' + return isUsernameRecipient ? 'INITIAL' : 'CONFIRM' + }) + + // store the initial charge id for fetching + const [chargeIdFromUrl] = useState(initialChargeId) + + // parsed url data + const [parsedUrl, setParsedUrl] = useState(initialParsedUrl) + + // track what came from url + const isAmountFromUrl = !!initialParsedUrl.amount + const isTokenFromUrl = !!initialParsedUrl.token + const isChainFromUrl = !!initialParsedUrl.chain + + // amount state + const [amount, setAmount] = useState(initialParsedUrl.amount || '') + const [usdAmount, setUsdAmount] = useState(initialParsedUrl.amount || '') + + // attachment state + const [attachment, setAttachment] = useState({}) + + // charge and payment results + const [charge, setCharge] = useState(null) + const [payment, setPayment] = useState(null) + const [txHash, setTxHash] = useState(null) + + // ui state + const [error, setError] = useState({ showError: false, errorMessage: '' }) + const [isLoading, setIsLoading] = useState(false) + const [isSuccess, setIsSuccess] = useState(false) + const [isExternalWalletPayment, setIsExternalWalletPayment] = useState(false) + + // derive recipient from parsed url + const recipient = useMemo(() => { + if (!parsedUrl?.recipient) return null + return { + identifier: parsedUrl.recipient.identifier, + recipientType: parsedUrl.recipient.recipientType, + resolvedAddress: parsedUrl.recipient.resolvedAddress as Address, + } + }, [parsedUrl]) + + // reset flow + const resetSemanticRequestFlow = useCallback(() => { + setCurrentView('INITIAL') + setAmount(initialParsedUrl.amount || '') + setUsdAmount(initialParsedUrl.amount || '') + setAttachment({}) + setCharge(null) + setPayment(null) + setTxHash(null) + setError({ showError: false, errorMessage: '' }) + setIsLoading(false) + setIsSuccess(false) + setIsExternalWalletPayment(false) + }, [initialParsedUrl.amount]) + + const value = useMemo( + () => ({ + currentView, + setCurrentView, + parsedUrl, + setParsedUrl, + recipient, + chargeIdFromUrl, + amount, + setAmount, + usdAmount, + setUsdAmount, + isAmountFromUrl, + isTokenFromUrl, + isChainFromUrl, + attachment, + setAttachment, + charge, + setCharge, + payment, + setPayment, + txHash, + setTxHash, + error, + setError, + isLoading, + setIsLoading, + isSuccess, + setIsSuccess, + resetSemanticRequestFlow, + isExternalWalletPayment, + setIsExternalWalletPayment, + }), + [ + currentView, + parsedUrl, + recipient, + chargeIdFromUrl, + amount, + usdAmount, + isAmountFromUrl, + isTokenFromUrl, + isChainFromUrl, + attachment, + charge, + payment, + txHash, + error, + isLoading, + isSuccess, + resetSemanticRequestFlow, + isExternalWalletPayment, + ] + ) + + return {children} +} + +export function useSemanticRequestFlowContext() { + const context = useContext(SemanticRequestFlowContext) + if (!context) { + throw new Error('useSemanticRequestFlowContext must be used within SemanticRequestFlowProvider') + } + return context +} diff --git a/src/features/payments/flows/semantic-request/SemanticRequestPage.tsx b/src/features/payments/flows/semantic-request/SemanticRequestPage.tsx new file mode 100644 index 000000000..af8ab5bc7 --- /dev/null +++ b/src/features/payments/flows/semantic-request/SemanticRequestPage.tsx @@ -0,0 +1,55 @@ +'use client' + +/** + * main entry point for semantic request flow + * + * wraps content with context provider and renders the correct view: + * - INITIAL: amount/token input + * - CONFIRM: review cross-chain payment details + * - STATUS: success view after payment + * - RECEIPT: shows receipt for already-paid charges + * + * receives pre-parsed url data from wrapper + */ + +import { SemanticRequestFlowProvider, useSemanticRequestFlowContext } from './SemanticRequestFlowContext' +import { SemanticRequestInputView } from './views/SemanticRequestInputView' +import { SemanticRequestConfirmView } from './views/SemanticRequestConfirmView' +import { SemanticRequestSuccessView } from './views/SemanticRequestSuccessView' +import { SemanticRequestReceiptView } from './views/SemanticRequestReceiptView' +import { type ParsedURL } from '@/lib/url-parser/types/payment' +import SemanticRequestExternalWalletView from './views/SemanticRequestExternalWalletView' + +// internal component that switches views +function SemanticRequestFlowContent() { + const { currentView } = useSemanticRequestFlowContext() + + switch (currentView) { + case 'CONFIRM': + return + case 'STATUS': + return + case 'RECEIPT': + return + case 'EXTERNAL_WALLET': + return + case 'INITIAL': + default: + return + } +} + +// props for the page +interface SemanticRequestPageProps { + parsedUrl: ParsedURL + initialChargeId?: string +} + +// exported page component with provider +export function SemanticRequestPage({ parsedUrl, initialChargeId }: SemanticRequestPageProps) { + return ( + + + + ) +} diff --git a/src/features/payments/flows/semantic-request/SemanticRequestPageWrapper.tsx b/src/features/payments/flows/semantic-request/SemanticRequestPageWrapper.tsx new file mode 100644 index 000000000..09ac39904 --- /dev/null +++ b/src/features/payments/flows/semantic-request/SemanticRequestPageWrapper.tsx @@ -0,0 +1,94 @@ +'use client' + +/** + * wrapper component for SemanticRequestPage + * + * handles async url parsing before rendering the actual flow. + * parses semantic urls like /username, /0x1234..., /vitalik.eth + * also supports amount/token/chain in url path + * + * shows loading/error states while parsing + * + * used by: /[...recipient] route for address/ens/username payments + */ + +import { SemanticRequestPage } from './SemanticRequestPage' +import { parsePaymentURL, type ParseUrlError } from '@/lib/url-parser/parser' +import PeanutLoading from '@/components/Global/PeanutLoading' +import ErrorAlert from '@/components/Global/ErrorAlert' +import NavHeader from '@/components/Global/NavHeader' +import { useRouter, useSearchParams } from 'next/navigation' +import { useEffect, useState } from 'react' +import { type ParsedURL } from '@/lib/url-parser/types/payment' +import { formatAmount } from '@/utils/general.utils' + +interface SemanticRequestPageWrapperProps { + recipient: string[] +} + +export function SemanticRequestPageWrapper({ recipient }: SemanticRequestPageWrapperProps) { + const router = useRouter() + const searchParams = useSearchParams() + const chargeIdFromUrl = searchParams.get('chargeId') + + const [parsedUrl, setParsedUrl] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + // parse the url segments + useEffect(() => { + if (!recipient || recipient.length === 0) { + setError({ message: 'Invalid URL format' } as ParseUrlError) + setIsLoading(false) + return + } + + setIsLoading(true) + setError(null) + + parsePaymentURL(recipient) + .then((result) => { + if (result.error) { + setError(result.error) + } else if (result.parsedUrl) { + // format amount if present + const formatted = { + ...result.parsedUrl, + amount: result.parsedUrl.amount ? formatAmount(result.parsedUrl.amount) : undefined, + } + setParsedUrl(formatted) + } + }) + .catch((err) => { + console.error('failed to parse url:', err) + setError({ message: 'Invalid URL format' } as ParseUrlError) + }) + .finally(() => { + setIsLoading(false) + }) + }, [recipient]) + + // loading state + if (isLoading) { + return ( +
+ router.back()} /> +
+ +
+
+ ) + } + + // error state + if (error || !parsedUrl) { + return ( +
+ router.back()} /> + +
+ ) + } + + return +} diff --git a/src/features/payments/flows/semantic-request/useSemanticRequestFlow.ts b/src/features/payments/flows/semantic-request/useSemanticRequestFlow.ts new file mode 100644 index 000000000..aebfdbad8 --- /dev/null +++ b/src/features/payments/flows/semantic-request/useSemanticRequestFlow.ts @@ -0,0 +1,606 @@ +'use client' + +/** + * hook for semantic request flow + * + * handles the full payment lifecycle for semantic url payments: + * 1. creates a charge with recipient/amount details + * 2. for same-chain usdc: sends directly via peanut wallet + * 3. for cross-chain/different token: calculates route, shows confirm view + * 4. executes swap/bridge transaction + * 5. records payment to backend + * + * supports route expiry handling - auto-refreshes routes when expired + */ + +import { useCallback, useMemo, useEffect, useContext, useState } from 'react' +import { type Address, type Hash } from 'viem' +import { useSemanticRequestFlowContext } from './SemanticRequestFlowContext' +import { useChargeManager } from '@/features/payments/shared/hooks/useChargeManager' +import { usePaymentRecorder } from '@/features/payments/shared/hooks/usePaymentRecorder' +import { useRouteCalculation } from '@/features/payments/shared/hooks/useRouteCalculation' +import { useWallet } from '@/hooks/wallet/useWallet' +import { useAuth } from '@/context/authContext' +import { tokenSelectorContext } from '@/context' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants/zerodev.consts' +import { ErrorHandler } from '@/utils/sdkErrorHandler.utils' +import { areEvmAddressesEqual } from '@/utils/general.utils' +import { useQueryClient } from '@tanstack/react-query' +import { TRANSACTIONS } from '@/constants/query.consts' + +export function useSemanticRequestFlow() { + const { + amount, + setAmount, + usdAmount, + setUsdAmount, + currentView, + setCurrentView, + parsedUrl, + recipient, + chargeIdFromUrl, + isAmountFromUrl, + isTokenFromUrl, + isChainFromUrl, + attachment, + setAttachment, + charge, + setCharge, + payment, + setPayment, + txHash, + setTxHash, + error, + setError, + isLoading, + setIsLoading, + isSuccess, + setIsSuccess, + resetSemanticRequestFlow, + isExternalWalletPayment, + setIsExternalWalletPayment, + } = useSemanticRequestFlowContext() + + const { user } = useAuth() + const queryClient = useQueryClient() + const { createCharge, fetchCharge, isCreating: isCreatingCharge, isFetching: isFetchingCharge } = useChargeManager() + const { recordPayment, isRecording } = usePaymentRecorder() + const { + route: calculatedRoute, + transactions: routeTransactions, + estimatedGasCostUsd: calculatedGasCost, + calculateRoute, + isCalculating: isCalculatingRoute, + isFeeEstimationError, + error: routeError, + reset: resetRoute, + } = useRouteCalculation() + const { + isConnected, + address: walletAddress, + sendMoney, + sendTransactions, + formattedBalance, + hasSufficientBalance, + } = useWallet() + + // use token selector context for ui integration + const { selectedChainID, selectedTokenAddress, selectedTokenData, setSelectedChainID, setSelectedTokenAddress } = + useContext(tokenSelectorContext) + + // route expiry state + const [isRouteExpired, setIsRouteExpired] = useState(false) + + const isLoggedIn = !!user?.user?.userId + + // set amount (for peanut wallet, amount is always in usd) + const handleSetAmount = useCallback( + (value: string) => { + setAmount(value) + setUsdAmount(value) + }, + [setAmount, setUsdAmount] + ) + + // clear error + const clearError = useCallback(() => { + setError({ showError: false, errorMessage: '' }) + }, [setError]) + + // check if payment is to peanut wallet token on peanut wallet chain (USDC on Arbitrum) + const isSameChainSameToken = useMemo(() => { + if (!selectedChainID || !selectedTokenAddress) return false + return ( + selectedChainID === PEANUT_WALLET_CHAIN.id.toString() && + areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN) + ) + }, [selectedChainID, selectedTokenAddress]) + + // check if this is cross-chain or different token + const isXChain = useMemo(() => { + if (!selectedChainID) return false + return selectedChainID !== PEANUT_WALLET_CHAIN.id.toString() + }, [selectedChainID]) + + const isDiffToken = useMemo(() => { + if (!selectedTokenAddress) return false + return !areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN) + }, [selectedTokenAddress]) + + // check if needs route (cross-chain or different token) + const needsRoute = isXChain || isDiffToken + + // check if can proceed to confirm/payment + const canProceed = useMemo(() => { + if (!amount || !recipient) return false + const amountNum = parseFloat(amount) + if (isNaN(amountNum) || amountNum <= 0) return false + if (!selectedTokenAddress || !selectedChainID) return false + return true + }, [amount, recipient, selectedTokenAddress, selectedChainID]) + + // check if has sufficient balance for current amount + const hasEnoughBalance = useMemo(() => { + if (!amount) return false + return hasSufficientBalance(amount) + }, [amount, hasSufficientBalance]) + + // check if should show insufficient balance error + const isInsufficientBalance = useMemo(() => { + return ( + isLoggedIn && + !!amount && + !hasEnoughBalance && + !isLoading && + !isCreatingCharge && + !isFetchingCharge && + !isRecording && + !isCalculatingRoute + ) + }, [ + isLoggedIn, + amount, + hasEnoughBalance, + isLoading, + isCreatingCharge, + isFetchingCharge, + isRecording, + isCalculatingRoute, + ]) + + // validate username recipient can only receive on arbitrum + const validateUsernameRecipient = useCallback((): string | null => { + if (recipient?.recipientType === 'USERNAME') { + // for username recipients, only arbitrum is allowed + if (selectedChainID && selectedChainID !== PEANUT_WALLET_CHAIN.id.toString()) { + return 'Payments to Peanut usernames can only be made on Arbitrum' + } + } + return null + }, [recipient?.recipientType, selectedChainID]) + + // update url with chargeId (shallow update - no re-render) + const updateUrlWithChargeId = useCallback((chargeId: string) => { + const currentUrl = new URL(window.location.href) + if (currentUrl.searchParams.get('chargeId') !== chargeId) { + currentUrl.searchParams.set('chargeId', chargeId) + window.history.replaceState({}, '', currentUrl.pathname + currentUrl.search) + } + }, []) + + // remove chargeId from url (shallow update - no re-render) + const removeChargeIdFromUrl = useCallback(() => { + const currentUrl = new URL(window.location.href) + if (currentUrl.searchParams.has('chargeId')) { + currentUrl.searchParams.delete('chargeId') + window.history.replaceState({}, '', currentUrl.pathname + currentUrl.search) + } + }, []) + + // handle payment button click - decides whether to skip confirm or not + // - if logged in + peanut wallet + same chain/token โ†’ create charge and pay directly + // - if logged in + peanut wallet + cross-chain/diff token โ†’ go to confirm view + // - if not logged in โ†’ action list handles it + const handlePayment = useCallback( + async ( + shouldReturnAfterCreatingCharge: boolean = false, + bypassLoginCheck: boolean = false + ): Promise<{ success: boolean }> => { + if (!recipient || !amount || !selectedTokenAddress || !selectedChainID || !selectedTokenData) { + setError({ showError: true, errorMessage: 'missing required data' }) + return { success: false } + } + + // validate username recipient + const validationError = validateUsernameRecipient() + if (validationError) { + setError({ showError: true, errorMessage: validationError }) + return { success: false } + } + + // if not logged in, don't proceed (action list handles this) + if (!bypassLoginCheck && (!isLoggedIn || !walletAddress)) { + setError({ showError: true, errorMessage: 'please log in to continue' }) + return { success: false } + } + + setIsLoading(true) + clearError() + + try { + // step 1: use existing charge if available (from url), otherwise create new one + let chargeResult = charge // use existing charge if loaded from chargeIdFromUrl + + if (!chargeResult) { + // only create new charge if we don't have one already + chargeResult = await createCharge({ + tokenAmount: amount, + tokenAddress: selectedTokenAddress as Address, + chainId: selectedChainID, + tokenSymbol: selectedTokenData.symbol, + tokenDecimals: selectedTokenData.decimals, + recipientAddress: recipient.resolvedAddress, + transactionType: 'REQUEST', + reference: attachment.message, + attachment: attachment.file, + currencyAmount: usdAmount, + currencyCode: 'USD', + }) + setCharge(chargeResult) + } + + if (shouldReturnAfterCreatingCharge) { + setIsLoading(false) + return { success: true } + } + + // step 2: decide flow based on token/chain + // if same chain and same token (USDC on Arb) โ†’ pay directly (skip confirm) + // if cross-chain or different token โ†’ go to confirm view + if (isSameChainSameToken) { + // direct payment - same as old flow when isPeanutWallet && same token/chain + const txResult = await sendMoney(recipient.resolvedAddress, amount) + const hash = (txResult.receipt?.transactionHash ?? txResult.userOpHash) as Hash + setTxHash(hash) + + // record payment + const paymentResult = await recordPayment({ + chargeId: chargeResult.uuid, + chainId: PEANUT_WALLET_CHAIN.id.toString(), + txHash: hash, + tokenAddress: PEANUT_WALLET_TOKEN as Address, + payerAddress: walletAddress as Address, + }) + + setPayment(paymentResult) + setIsSuccess(true) + setCurrentView('STATUS') + + // refetch history and balance to immediately show updated status + // invalidate first to mark as stale, then refetch to force immediate update + queryClient.invalidateQueries({ queryKey: [TRANSACTIONS] }) + queryClient.refetchQueries({ + queryKey: [TRANSACTIONS], + type: 'active', // force refetch even if data is fresh + }) + queryClient.invalidateQueries({ queryKey: ['balance'] }) + } else { + // cross-chain or different token โ†’ go to confirm view + // update url with chargeId + updateUrlWithChargeId(chargeResult.uuid) + setCurrentView('CONFIRM') + } + setIsLoading(false) + return { success: true } + } catch (err) { + const errorMessage = ErrorHandler(err) + setError({ showError: true, errorMessage }) + setIsLoading(false) + return { success: false } + } + }, + [ + recipient, + amount, + usdAmount, + attachment, + walletAddress, + selectedTokenAddress, + selectedChainID, + selectedTokenData, + charge, + isLoggedIn, + isSameChainSameToken, + validateUsernameRecipient, + createCharge, + sendMoney, + recordPayment, + queryClient, + updateUrlWithChargeId, + setCharge, + setTxHash, + setPayment, + setIsSuccess, + setCurrentView, + setError, + setIsLoading, + clearError, + ] + ) + + // prepare route when entering confirm view + const prepareRoute = useCallback(async () => { + if (!charge || !walletAddress || !selectedTokenData || !selectedChainID) return + + setIsRouteExpired(false) + + // check if charge is for same chain and same token (no route needed) + const isChargeSameChainToken = + charge.chainId === PEANUT_WALLET_CHAIN.id.toString() && + areEvmAddressesEqual(charge.tokenAddress, PEANUT_WALLET_TOKEN) + + // only calculate route if cross-chain or different token + if (needsRoute && !isChargeSameChainToken) { + await calculateRoute({ + source: { + address: walletAddress as Address, + tokenAddress: PEANUT_WALLET_TOKEN as Address, + chainId: PEANUT_WALLET_CHAIN.id.toString(), + }, + destination: { + recipientAddress: charge.requestLink.recipientAddress as Address, + tokenAddress: charge.tokenAddress as Address, + tokenAmount: charge.tokenAmount, + tokenDecimals: charge.tokenDecimals, + tokenType: 1, // ERC20 + chainId: charge.chainId, + }, + usdAmount: usdAmount || amount, + }) + } + }, [charge, walletAddress, selectedTokenData, selectedChainID, needsRoute, calculateRoute, usdAmount, amount]) + + // fetch charge from url if chargeIdFromUrl is present but charge is not loaded + useEffect(() => { + if ( + chargeIdFromUrl && + !charge && + (currentView === 'INITIAL' || currentView === 'CONFIRM' || currentView === 'RECEIPT') && + !isFetchingCharge + ) { + fetchCharge(chargeIdFromUrl) + .then((fetchedCharge) => { + setCharge(fetchedCharge) + + // check if charge is already paid - if so, switch to receipt view + const isPaid = fetchedCharge.fulfillmentPayment?.status === 'SUCCESSFUL' + if (isPaid && (currentView === 'CONFIRM' || currentView === 'INITIAL')) { + setCurrentView('RECEIPT') + return + } + + // set amount from charge if not already set + if (!amount && fetchedCharge.tokenAmount) { + setAmount(fetchedCharge.tokenAmount) + setUsdAmount(fetchedCharge.currencyAmount || fetchedCharge.tokenAmount) + } + // set token/chain from charge for token selector context + if (fetchedCharge.chainId) { + setSelectedChainID(fetchedCharge.chainId) + } + if (fetchedCharge.tokenAddress) { + setSelectedTokenAddress(fetchedCharge.tokenAddress) + } + }) + .catch((err) => { + console.error('failed to fetch charge:', err) + setError({ showError: true, errorMessage: 'failed to load payment details' }) + }) + } + }, [ + chargeIdFromUrl, + charge, + currentView, + isFetchingCharge, + fetchCharge, + setCharge, + amount, + setAmount, + setUsdAmount, + setError, + setSelectedChainID, + setSelectedTokenAddress, + setCurrentView, + ]) + + // call prepareRoute when entering confirm view and charge is ready + useEffect(() => { + if (currentView === 'CONFIRM' && charge) { + prepareRoute() + } + }, [currentView, charge, prepareRoute]) + + // handle route expiry - sets state, useEffect will trigger refetch + const handleRouteExpired = useCallback(() => { + setIsRouteExpired(true) + }, []) + + // auto-refetch route when expired + useEffect(() => { + if (isRouteExpired && currentView === 'CONFIRM' && !isLoading && !isCalculatingRoute) { + prepareRoute() + } + }, [isRouteExpired, currentView, isLoading, isCalculatingRoute, prepareRoute]) + + // handle route near expiry - refetch immediately + const handleRouteNearExpiry = useCallback(() => { + if (!isLoading && !isCalculatingRoute) { + prepareRoute() + } + }, [isLoading, isCalculatingRoute, prepareRoute]) + + // execute payment from confirm view (handles both same-chain and cross-chain) + const executePayment = useCallback(async () => { + if (!recipient || !amount || !walletAddress || !charge) { + setError({ showError: true, errorMessage: 'missing required data' }) + return + } + + setIsLoading(true) + clearError() + + try { + let hash: Hash + + // check if charge is for same chain and same token (usdc on arbitrum) + const isChargeSameChainToken = + charge.chainId === PEANUT_WALLET_CHAIN.id.toString() && + areEvmAddressesEqual(charge.tokenAddress, PEANUT_WALLET_TOKEN) + + if (isChargeSameChainToken) { + // direct payment for same-chain same-token (e.g. direct requests) + const txResult = await sendMoney(charge.requestLink.recipientAddress as Address, charge.tokenAmount) + hash = (txResult.receipt?.transactionHash ?? txResult.userOpHash) as Hash + } else if (needsRoute && routeTransactions && routeTransactions.length > 0) { + // cross-chain or token swap payment via squid route + const txResult = await sendTransactions( + routeTransactions.map((tx) => ({ + to: tx.to, + data: tx.data, + value: tx.value, + })) + ) + hash = (txResult.receipt?.transactionHash ?? txResult.userOpHash) as Hash + } else { + throw new Error('route not ready for cross-chain payment') + } + + setTxHash(hash) + + // record payment to backend + const paymentResult = await recordPayment({ + chargeId: charge.uuid, + chainId: PEANUT_WALLET_CHAIN.id.toString(), + txHash: hash, + tokenAddress: PEANUT_WALLET_TOKEN as Address, + payerAddress: walletAddress as Address, + sourceChainId: selectedChainID || undefined, + sourceTokenAddress: selectedTokenAddress || undefined, + sourceTokenSymbol: selectedTokenData?.symbol, + }) + + setPayment(paymentResult) + setIsSuccess(true) + setCurrentView('STATUS') + + // refetch history and balance to immediately show updated status + // invalidate first to mark as stale, then refetch to force immediate update + queryClient.invalidateQueries({ queryKey: [TRANSACTIONS] }) + queryClient.refetchQueries({ + queryKey: [TRANSACTIONS], + type: 'active', // force refetch even if data is fresh + }) + queryClient.invalidateQueries({ queryKey: ['balance'] }) + } catch (err) { + const errorMessage = ErrorHandler(err) + setError({ showError: true, errorMessage }) + } finally { + setIsLoading(false) + } + }, [ + recipient, + amount, + walletAddress, + charge, + needsRoute, + routeTransactions, + selectedChainID, + selectedTokenAddress, + selectedTokenData, + sendMoney, + sendTransactions, + recordPayment, + queryClient, + setTxHash, + setPayment, + setIsSuccess, + setCurrentView, + setError, + setIsLoading, + clearError, + ]) + + // go back from confirm to initial + const goBackToInitial = useCallback(() => { + setCurrentView('INITIAL') + setCharge(null) + resetRoute() + setIsRouteExpired(false) + removeChargeIdFromUrl() + }, [setCurrentView, setCharge, resetRoute, removeChargeIdFromUrl]) + + return { + // state + amount, + usdAmount, + currentView, + parsedUrl, + recipient, + chargeIdFromUrl, + isAmountFromUrl, + isTokenFromUrl, + isChainFromUrl, + attachment, + charge, + payment, + txHash, + error, + isLoading: isLoading || isCreatingCharge || isFetchingCharge || isRecording || isCalculatingRoute, + isSuccess, + isFetchingCharge, + isExternalWalletPayment, + + // route calculation state (for confirm view) + calculatedRoute, + routeTransactions, + calculatedGasCost, + isCalculatingRoute, + isFeeEstimationError, + routeError, + isRouteExpired, + + // computed + canProceed, + hasSufficientBalance: hasEnoughBalance, + isInsufficientBalance, + isConnected, + isLoggedIn, + walletAddress, + formattedBalance, + isXChain, + isDiffToken, + needsRoute, + isSameChainSameToken, + + // token selector (from context for ui) + selectedChainID, + selectedTokenAddress, + selectedTokenData, + setSelectedChainID, + setSelectedTokenAddress, + + // actions + setAmount: handleSetAmount, + setAttachment, + clearError, + handlePayment, + prepareRoute, + executePayment, + goBackToInitial, + resetSemanticRequestFlow, + setCurrentView, + handleRouteExpired, + handleRouteNearExpiry, + setIsExternalWalletPayment, + } +} diff --git a/src/features/payments/flows/semantic-request/views/SemanticRequestConfirmView.tsx b/src/features/payments/flows/semantic-request/views/SemanticRequestConfirmView.tsx new file mode 100644 index 000000000..b9bac7545 --- /dev/null +++ b/src/features/payments/flows/semantic-request/views/SemanticRequestConfirmView.tsx @@ -0,0 +1,311 @@ +'use client' + +/** + * confirm view for semantic request flow (cross-chain payments only) + * + * displays: + * - recipient and amount being sent + * - min received after slippage + * - source token (usdc on arb) โ†’ destination token + * - network fees (usually sponsored by peanut) + * - countdown timer for rfq routes (auto-refreshes before expiry) + * + * handles route expiry - auto-fetches new quote when current expires + */ + +import { Button } from '@/components/0_Bruddle/Button' +import Card from '@/components/Global/Card' +import NavHeader from '@/components/Global/NavHeader' +import ErrorAlert from '@/components/Global/ErrorAlert' +import PeanutLoading from '@/components/Global/PeanutLoading' +import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow' +import DisplayIcon from '@/components/Global/DisplayIcon' +import { useSemanticRequestFlow } from '../useSemanticRequestFlow' +import { formatAmount, isStableCoin } from '@/utils/general.utils' +import { useTokenChainIcons } from '@/hooks/useTokenChainIcons' +import { useMemo } from 'react' +import { formatUnits } from 'viem' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN, PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants/zerodev.consts' +import PeanutActionDetailsCard, { + type PeanutActionDetailsCardRecipientType, +} from '@/components/Global/PeanutActionDetailsCard' + +export function SemanticRequestConfirmView() { + const { + amount, + usdAmount, + recipient, + charge, + attachment, + error, + calculatedRoute, + calculatedGasCost, + isCalculatingRoute, + isFeeEstimationError, + routeError, + isXChain, + isDiffToken, + isLoading, + isFetchingCharge, + selectedChainID, + selectedTokenData, + goBackToInitial, + executePayment, + prepareRoute, + handleRouteExpired, + handleRouteNearExpiry, + } = useSemanticRequestFlow() + + // icons for sending token (peanut wallet usdc) + const { + tokenIconUrl: sendingTokenIconUrl, + chainIconUrl: sendingChainIconUrl, + resolvedChainName: sendingResolvedChainName, + resolvedTokenSymbol: sendingResolvedTokenSymbol, + } = useTokenChainIcons({ + chainId: PEANUT_WALLET_CHAIN.id.toString(), + tokenAddress: PEANUT_WALLET_TOKEN, + tokenSymbol: PEANUT_WALLET_TOKEN_SYMBOL, + }) + + // icons for requested/destination token + const { + tokenIconUrl: requestedTokenIconUrl, + chainIconUrl: requestedChainIconUrl, + resolvedChainName: requestedResolvedChainName, + resolvedTokenSymbol: requestedResolvedTokenSymbol, + } = useTokenChainIcons({ + chainId: charge?.chainId, + tokenAddress: charge?.tokenAddress, + tokenSymbol: charge?.tokenSymbol, + }) + + // is cross-chain or different token + const isCrossChainPayment = isXChain || isDiffToken + + // format display values + const displayAmount = useMemo(() => { + return `${formatAmount(usdAmount || amount)}` + }, [amount, usdAmount]) + + // get network fee display + const networkFee = useMemo(() => { + if (isFeeEstimationError) return '-' + if (calculatedGasCost === undefined) { + return 'Sponsored by Peanut!' + } + if (calculatedGasCost < 0.01) { + return 'Sponsored by Peanut!' + } + return ( + <> + $ {calculatedGasCost.toFixed(2)} + {' - '} + Sponsored by Peanut! + + ) + }, [calculatedGasCost, isFeeEstimationError]) + + // min received amount + const minReceived = useMemo(() => { + if (!charge?.tokenDecimals || !requestedResolvedTokenSymbol) return null + if (!calculatedRoute) { + return `$ ${charge?.tokenAmount}` + } + const amount = formatAmount( + formatUnits(BigInt(calculatedRoute.rawResponse.route.estimate.toAmountMin), charge.tokenDecimals) + ) + return isStableCoin(requestedResolvedTokenSymbol) ? `$ ${amount}` : `${amount} ${requestedResolvedTokenSymbol}` + }, [calculatedRoute, charge?.tokenDecimals, charge?.tokenAmount, requestedResolvedTokenSymbol]) + + // error message (route expiry auto-retries) + const errorMessage = useMemo(() => { + if (routeError) return routeError + if (error.showError) return error.errorMessage + return null + }, [routeError, error]) + + // handle confirm + const handleConfirm = () => { + if (!isLoading && !isCalculatingRoute) { + executePayment() + } + } + + // handle retry + const handleRetry = async () => { + if (errorMessage) { + // retry route calculation + await prepareRoute() + } else { + await executePayment() + } + } + + // show loading if we don't have charge details yet or fetching + if (!charge || isFetchingCharge) { + return ( +
+ +
+ ) + } + + return ( +
+ + +
+ {recipient && recipient.recipientType && ( + + )} + {/* payment details card */} + + + + {isCrossChainPayment && ( + + } + /> + )} + + + } + /> + + + + + + + {/* buttons and error */} +
+ {errorMessage ? ( + + ) : ( + + )} + {errorMessage && ( +
+ +
+ )} +
+
+
+ ) +} + +// helper component for token/chain display +interface TokenChainInfoDisplayProps { + tokenIconUrl?: string + chainIconUrl?: string + resolvedTokenSymbol?: string + fallbackTokenSymbol: string + resolvedChainName?: string + fallbackChainName: string +} + +function TokenChainInfoDisplay({ + tokenIconUrl, + chainIconUrl, + resolvedTokenSymbol, + fallbackTokenSymbol, + resolvedChainName, + fallbackChainName, +}: TokenChainInfoDisplayProps) { + const tokenSymbol = resolvedTokenSymbol || fallbackTokenSymbol + const chainName = resolvedChainName || fallbackChainName + + return ( +
+ {(tokenIconUrl || chainIconUrl) && ( +
+ {tokenIconUrl && ( + + )} + {chainIconUrl && ( +
+ +
+ )} +
+ )} + + {tokenSymbol} on {chainName} + +
+ ) +} diff --git a/src/features/payments/flows/semantic-request/views/SemanticRequestExternalWalletView.tsx b/src/features/payments/flows/semantic-request/views/SemanticRequestExternalWalletView.tsx new file mode 100644 index 000000000..526126b0f --- /dev/null +++ b/src/features/payments/flows/semantic-request/views/SemanticRequestExternalWalletView.tsx @@ -0,0 +1,51 @@ +import { useCallback, useState } from 'react' +import { useSemanticRequestFlow } from '../useSemanticRequestFlow' +import { useWallet } from '@/hooks/wallet/useWallet' +import { useQuery } from '@tanstack/react-query' +import { rhinoApi } from '@/services/rhino' +import RhinoDepositView from '@/components/AddMoney/views/RhinoDeposit.view' +import type { RhinoChainType } from '@/services/services.types' + +const SemanticRequestExternalWalletView = () => { + const { charge, setCurrentView, setIsExternalWalletPayment, amount } = useSemanticRequestFlow() + const [chainType, setChainType] = useState('EVM') + const { address: peanutWalletAddress } = useWallet() + + const { data: depositAddressData, isLoading } = useQuery({ + queryKey: ['rhino-deposit-address', charge?.uuid, chainType], + queryFn: () => { + if (!charge?.uuid) { + throw new Error('Charge ID is required') + } + return rhinoApi.createRequestFulfilmentAddress(chainType, charge?.uuid as string, peanutWalletAddress) + }, + enabled: !!charge, + staleTime: 1000 * 60 * 60 * 24, // 24 hours + }) + + const onSuccess = useCallback((_: number) => { + setIsExternalWalletPayment(true) + setCurrentView('STATUS') + }, []) + + return ( + setCurrentView('INITIAL')} + showUserCard + amount={Number(amount)} + identifier={ + charge?.requestLink.recipientAccount.type === 'peanut-wallet' + ? charge?.requestLink.recipientAccount.user.username + : charge?.requestLink.recipientAccount.identifier + } + /> + ) +} + +export default SemanticRequestExternalWalletView diff --git a/src/features/payments/flows/semantic-request/views/SemanticRequestInputView.tsx b/src/features/payments/flows/semantic-request/views/SemanticRequestInputView.tsx new file mode 100644 index 000000000..f24646be7 --- /dev/null +++ b/src/features/payments/flows/semantic-request/views/SemanticRequestInputView.tsx @@ -0,0 +1,207 @@ +'use client' + +/** + * input view for semantic request flow + * + * displays: + * - recipient card (address/ens/username) + * - amount input + * - token selector (for address/ens recipients, not usernames) + * - payment method options + * + * for same-chain usdc: executes payment directly + * for cross-chain: navigates to confirm view + */ + +import { useEffect, useContext } from 'react' +import NavHeader from '@/components/Global/NavHeader' +import AmountInput from '@/components/Global/AmountInput' +import UserCard from '@/components/User/UserCard' +import ErrorAlert from '@/components/Global/ErrorAlert' +import SupportCTA from '@/components/Global/SupportCTA' +import TokenSelector from '@/components/Global/TokenSelector/TokenSelector' +import { useSemanticRequestFlow } from '../useSemanticRequestFlow' +import { useRouter } from 'next/navigation' +import SendWithPeanutCta from '@/features/payments/shared/components/SendWithPeanutCta' +import { PaymentMethodActionList } from '@/features/payments/shared/components/PaymentMethodActionList' +import { printableAddress, areEvmAddressesEqual } from '@/utils/general.utils' +import { tokenSelectorContext } from '@/context' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants/zerodev.consts' + +export function SemanticRequestInputView() { + const router = useRouter() + const { + amount, + recipient, + parsedUrl, + chargeIdFromUrl, + isAmountFromUrl, + error, + formattedBalance, + canProceed, + isInsufficientBalance, + isLoading, + isLoggedIn, + isConnected, + setAmount, + handlePayment, + setCurrentView, + } = useSemanticRequestFlow() + + // token selector context for setting initial values from url + const { + setSelectedChainID, + setSelectedTokenAddress, + selectedChainID, + selectedTokenAddress, + supportedSquidChainsAndTokens, + selectedTokenData, + } = useContext(tokenSelectorContext) + + // initialize token/chain from parsed url + useEffect(() => { + if (!parsedUrl) return + + // set chain from url if available + if (parsedUrl.chain?.chainId) { + setSelectedChainID(parsedUrl.chain.chainId) + } else { + // default to arbitrum for external recipients + setSelectedChainID(PEANUT_WALLET_CHAIN.id.toString()) + } + + // set token from url if available + if (parsedUrl.token?.address) { + setSelectedTokenAddress(parsedUrl.token.address) + } else if (parsedUrl.chain?.chainId) { + // default to usdc on the selected chain + const chainData = supportedSquidChainsAndTokens[parsedUrl.chain.chainId] + const defaultToken = chainData?.tokens.find((t) => t.symbol.toLowerCase() === 'usdc') + if (defaultToken) { + setSelectedTokenAddress(defaultToken.address) + } + } else { + // default to peanut wallet usdc + setSelectedTokenAddress(PEANUT_WALLET_TOKEN) + } + }, [parsedUrl, setSelectedChainID, setSelectedTokenAddress, supportedSquidChainsAndTokens]) + + // handle submit + const handleSubmit = () => { + if (canProceed && !isLoading) { + handlePayment() + } + } + + const handleOpenExternalWalletFlow = async () => { + if (canProceed && !isLoading) { + const res = await handlePayment(true, true) // return after creating charge + // Proceed only if charge is created successfully + if (res && res.success) { + setCurrentView('EXTERNAL_WALLET') + } + } + } + + // handle back navigation + const handleGoBack = () => { + if (window.history.length > 1) { + router.back() + } else { + router.push('/') + } + } + + // determine button state + const isButtonDisabled = !canProceed || isLoading + const isAmountEntered = !!amount && parseFloat(amount) > 0 + + // get display name for recipient + const recipientDisplayName = + recipient?.recipientType === 'ADDRESS' + ? printableAddress(recipient.resolvedAddress) + : recipient?.identifier || '' + + // check if using peanut wallet default (usdc on arb) + const isUsingPeanutDefault = + selectedChainID === PEANUT_WALLET_CHAIN.id.toString() && + areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN) + + // determine if we should show token selector + // only show when chain is NOT specified in url AND recipient is ADDRESS or ENS + const showTokenSelector = + !parsedUrl?.chain?.chainId && + (recipient?.recipientType === 'ADDRESS' || recipient?.recipientType === 'ENS') && + isConnected + + return ( +
+ + +
+ {/* recipient card */} + {recipient && ( + + )} + + {/* amount input */} + + + {/* token selector for chain/token selection (not for USERNAME) */} + {showTokenSelector && } + + {/* hint for free transactions */} + {showTokenSelector && selectedTokenAddress && selectedChainID && !isUsingPeanutDefault && ( +
+ Use USDC on Arbitrum for free transactions! +
+ )} + + {/* button and error */} +
+ + {isInsufficientBalance && ( + + )} + {error.showError && } +
+ + {/* action list for non-logged in users */} + +
+ + {/* support cta */} + {!isLoggedIn && } +
+ ) +} diff --git a/src/features/payments/flows/semantic-request/views/SemanticRequestReceiptView.tsx b/src/features/payments/flows/semantic-request/views/SemanticRequestReceiptView.tsx new file mode 100644 index 000000000..e1b1f91c3 --- /dev/null +++ b/src/features/payments/flows/semantic-request/views/SemanticRequestReceiptView.tsx @@ -0,0 +1,131 @@ +'use client' + +/** + * receipt view for semantic request flow + * + * displays transaction receipt when visiting a charge url that's already been paid + * uses TransactionDetailsReceipt to show full payment details + */ + +import { TransactionDetailsReceipt } from '@/components/TransactionDetails/TransactionDetailsReceipt' +import { type TransactionDetails } from '@/components/TransactionDetails/transactionTransformer' +import { useSemanticRequestFlow } from '../useSemanticRequestFlow' +import { useMemo } from 'react' +import { type StatusPillType } from '@/components/Global/StatusPill' +import { EHistoryEntryType, EHistoryUserRole } from '@/hooks/useTransactionHistory' +import { getInitialsFromName } from '@/utils/general.utils' +import { BASE_URL } from '@/constants/general.consts' +import { useTokenChainIcons } from '@/hooks/useTokenChainIcons' +import PeanutLoading from '@/components/Global/PeanutLoading' +import NavHeader from '@/components/Global/NavHeader' +import { useRouter } from 'next/navigation' + +export function SemanticRequestReceiptView() { + const router = useRouter() + const { charge, recipient, parsedUrl, isFetchingCharge } = useSemanticRequestFlow() + + const { tokenIconUrl, chainIconUrl, resolvedChainName, resolvedTokenSymbol } = useTokenChainIcons({ + chainId: charge?.chainId, + tokenSymbol: charge?.tokenSymbol, + tokenAddress: charge?.tokenAddress, + }) + + // construct transaction details for receipt + const transactionForReceipt: TransactionDetails | null = useMemo(() => { + if (!charge) return null + + // check if charge has been fulfilled + const isPaid = charge.fulfillmentPayment?.status === 'SUCCESSFUL' + if (!isPaid) return null + + // get the successful payment for payer details + const successfulPayment = charge.payments?.find((p) => p.status === 'SUCCESSFUL') + if (!successfulPayment) return null + + const recipientIdentifier = recipient?.identifier || parsedUrl?.recipient?.identifier + const receiptLink = recipientIdentifier + ? `${BASE_URL}/${recipientIdentifier}?chargeId=${charge.uuid}` + : undefined + + const networkFeeDisplayValue = '$ 0.00' // fee is zero for peanut wallet txns + const peanutFeeDisplayValue = '$ 0.00' // peanut doesn't charge fees yet + + // determine who paid (payer name for display) + const payerName = successfulPayment.payerAccount?.user?.username || successfulPayment.payerAddress || 'Unknown' + + const details: Partial = { + id: successfulPayment.payerTransactionHash || charge.uuid, + txHash: successfulPayment.payerTransactionHash, + status: 'completed' as StatusPillType, + amount: parseFloat(charge.tokenAmount), + createdAt: new Date(charge.createdAt), + completedAt: new Date(successfulPayment.createdAt), + tokenSymbol: charge.tokenSymbol, + direction: 'receive', // showing receipt from recipient's perspective + initials: getInitialsFromName(payerName), + extraDataForDrawer: { + isLinkTransaction: false, + originalType: EHistoryEntryType.REQUEST, + originalUserRole: EHistoryUserRole.RECIPIENT, + link: receiptLink, + }, + userName: payerName, + sourceView: 'status', + memo: charge.requestLink?.reference || undefined, + attachmentUrl: charge.requestLink?.attachmentUrl || undefined, + tokenDisplayDetails: { + tokenSymbol: resolvedTokenSymbol || charge.tokenSymbol, + chainName: resolvedChainName, + tokenIconUrl: tokenIconUrl, + chainIconUrl: chainIconUrl, + }, + networkFeeDetails: { + amountDisplay: networkFeeDisplayValue, + moreInfoText: 'This transaction may face slippage due to token conversion or cross-chain bridging.', + }, + peanutFeeDetails: { + amountDisplay: peanutFeeDisplayValue, + }, + currency: charge.currencyAmount ? { amount: charge.currencyAmount, code: 'USD' } : undefined, + } + + return details as TransactionDetails + }, [charge, recipient, parsedUrl, tokenIconUrl, chainIconUrl, resolvedChainName, resolvedTokenSymbol]) + + // show loading if fetching charge + if (isFetchingCharge || !charge) { + return ( +
+ router.back()} /> +
+ +
+
+ ) + } + + // show receipt if we have transaction details + if (!transactionForReceipt) { + return ( +
+ router.back()} /> +
+

Unable to load receipt details

+
+
+ ) + } + + return ( +
+ router.back()} /> +
+ +
+
+ ) +} diff --git a/src/features/payments/flows/semantic-request/views/SemanticRequestSuccessView.tsx b/src/features/payments/flows/semantic-request/views/SemanticRequestSuccessView.tsx new file mode 100644 index 000000000..bbd41286f --- /dev/null +++ b/src/features/payments/flows/semantic-request/views/SemanticRequestSuccessView.tsx @@ -0,0 +1,55 @@ +'use client' + +/** + * success view for semantic request flow + * + * thin wrapper around PaymentSuccessView that: + * - pulls data from semantic request flow context + * - calculates points earned for the payment + * - provides reset callback on completion + */ + +import PaymentSuccessView from '@/features/payments/shared/components/PaymentSuccessView' +import { useSemanticRequestFlow } from '../useSemanticRequestFlow' +import { usePointsCalculation } from '@/hooks/usePointsCalculation' +import { PointsAction } from '@/services/services.types' + +export function SemanticRequestSuccessView() { + const { + usdAmount, + recipient, + parsedUrl, + attachment, + charge, + payment, + resetSemanticRequestFlow, + isExternalWalletPayment, + } = useSemanticRequestFlow() + + // determine recipient type from parsed url + const recipientType = recipient?.recipientType || 'ADDRESS' + + // calculate points for the payment (request fulfillment) + const { pointsData } = usePointsCalculation( + PointsAction.P2P_REQUEST_PAYMENT, + usdAmount, + !!payment || isExternalWalletPayment, // For external wallet payments, we dont't have payment info on the FE, its handled by webooks on BE + payment?.uuid + ) + + return ( + + ) +} diff --git a/src/features/payments/shared/components/PaymentMethodActionList.tsx b/src/features/payments/shared/components/PaymentMethodActionList.tsx new file mode 100644 index 000000000..ba9a7f3cd --- /dev/null +++ b/src/features/payments/shared/components/PaymentMethodActionList.tsx @@ -0,0 +1,120 @@ +'use client' + +/** + * payment method action list for payment flows + * + * shows alternative payment methods (bank, mercadopago, pix) + * for users who don't have peanut wallet balance. + * + * redirects to add-money flow after login/signup + * + * used by: send, semantic-request input views + */ + +import { useRouter } from 'next/navigation' +import Divider from '@/components/0_Bruddle/Divider' +import { ActionListCard } from '@/components/ActionListCard' +import IconStack from '@/components/Global/IconStack' +import StatusBadge from '@/components/Global/Badges/StatusBadge' +import { ACTION_METHODS, type PaymentMethod } from '@/constants/actionlist.consts' +import { useGeoFilteredPaymentOptions } from '@/hooks/useGeoFilteredPaymentOptions' +import Loading from '@/components/Global/Loading' +import useKycStatus from '@/hooks/useKycStatus' +import { saveRedirectUrl } from '@/utils/general.utils' + +interface PaymentMethodActionListProps { + isAmountEntered: boolean + showDivider?: boolean + onPayWithExternalWallet?: () => void +} + +/** + * generic payment method action list for both direct send and semantic request flows + * shows bank/mercadopago/pix options + * redirects to setup with add-money as final destination after login/signup if user is not logged in + * @param isAmountEntered - whether the amount is entered + * @param showDivider - whether to show the divider + * @returns the payment options list component + */ + +export function PaymentMethodActionList({ + isAmountEntered, + showDivider = true, + onPayWithExternalWallet, +}: PaymentMethodActionListProps) { + const router = useRouter() + const { isUserMantecaKycApproved, isUserBridgeKycApproved } = useKycStatus() + + // use geo filtering hook to sort methods based on user location + // note: we don't mark verification-required methods as unavailable - they're still clickable + const { filteredMethods: sortedMethods, isLoading: isGeoLoading } = useGeoFilteredPaymentOptions({ + sortUnavailable: true, + isMethodUnavailable: (method) => method.soon, + methods: ACTION_METHODS, + }) + + const handleMethodClick = (method: PaymentMethod) => { + // for all methods, save current url and redirect to setup with add-money as final destination + // verification will be handled in the add-money flow after login + + if (method.id === 'exchange-or-wallet' && onPayWithExternalWallet) { + onPayWithExternalWallet() + return + } + + if (['bank', 'mercadopago', 'pix'].includes(method.id)) { + saveRedirectUrl() + const redirectUri = encodeURIComponent('/add-money') + router.push(`/setup?redirect_uri=${redirectUri}`) + } + } + + if (isGeoLoading) { + return ( +
+ +
+ ) + } + + return ( +
+ {showDivider && } +
+ {sortedMethods.map((method) => { + // check if method requires verification (for badge display only) + const methodRequiresMantecaVerification = + ['mercadopago', 'pix'].includes(method.id) && !isUserMantecaKycApproved + const methodRequiresBridgeVerification = method.id === 'bank' && !isUserBridgeKycApproved + const methodRequiresVerification = + methodRequiresMantecaVerification || methodRequiresBridgeVerification + return ( + + {method.title} + {(method.soon || methodRequiresVerification) && ( + + )} +
+ } + onClick={() => handleMethodClick(method)} + isDisabled={method.soon || !isAmountEntered} + rightContent={} + /> + ) + })} +
+
+ ) +} + +// re-export for backward compatibility +export { PaymentMethodActionList as SendActionList } diff --git a/src/components/Payment/Views/Status.payment.view.tsx b/src/features/payments/shared/components/PaymentSuccessView.tsx similarity index 87% rename from src/components/Payment/Views/Status.payment.view.tsx rename to src/features/payments/shared/components/PaymentSuccessView.tsx index c8b916d61..8789657d1 100644 --- a/src/components/Payment/Views/Status.payment.view.tsx +++ b/src/features/payments/shared/components/PaymentSuccessView.tsx @@ -1,5 +1,19 @@ 'use client' -import { Button } from '@/components/0_Bruddle' + +/** + * shared success view for all payment flows + * + * displays: + * - success animation with peanut mascot + * - amount sent and recipient name + * - optional message/attachment + * - points earned (with confetti) + * - receipt drawer for transaction details + * + * used by: send, contribute-pot, semantic-request, withdraw flows + */ + +import { Button } from '@/components/0_Bruddle/Button' import AddressLink from '@/components/Global/AddressLink' import Card from '@/components/Global/Card' import CreateAccountButton from '@/components/Global/CreateAccountButton' @@ -9,32 +23,37 @@ import { SoundPlayer } from '@/components/Global/SoundPlayer' import { type StatusPillType } from '@/components/Global/StatusPill' import { TransactionDetailsDrawer } from '@/components/TransactionDetails/TransactionDetailsDrawer' import { type TransactionDetails } from '@/components/TransactionDetails/transactionTransformer' -import { TRANSACTIONS, BASE_URL } from '@/constants' import { useTokenChainIcons } from '@/hooks/useTokenChainIcons' import { useTransactionDetailsDrawer } from '@/hooks/useTransactionDetailsDrawer' import { EHistoryEntryType, EHistoryUserRole } from '@/hooks/useTransactionHistory' import { type RecipientType } from '@/lib/url-parser/types/payment' -import { usePaymentStore, useUserStore } from '@/redux/hooks' -import { paymentActions } from '@/redux/slices/payment-slice' -import { type ApiUser } from '@/services/users' -import { formatAmount, getInitialsFromName, printableAddress } from '@/utils' +import { useUserStore } from '@/redux/hooks' +import type { TRequestChargeResponse, PaymentCreationResponse, ChargeEntry } from '@/services/services.types' +import { formatAmount, getInitialsFromName, printableAddress } from '@/utils/general.utils' import { useQueryClient } from '@tanstack/react-query' import Image from 'next/image' import { useRouter } from 'next/navigation' import { type ReactNode, useEffect, useMemo, useRef } from 'react' -import { useDispatch } from 'react-redux' -import STAR_STRAIGHT_ICON from '@/assets/icons/starStraight.svg' import { usePointsConfetti } from '@/hooks/usePointsConfetti' import chillPeanutAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_01.gif' import { useHaptic } from 'use-haptic' import PointsCard from '@/components/Common/PointsCard' +import { BASE_URL } from '@/constants/general.consts' +import { TRANSACTIONS } from '@/constants/query.consts' +import type { ParsedURL } from '@/lib/url-parser/types/payment' + +// minimal user info needed for display +type UserDisplayInfo = { + username?: string + fullName?: string +} type DirectSuccessViewProps = { - user?: ApiUser + user?: UserDisplayInfo amount?: string message?: string | ReactNode recipientType?: RecipientType - type?: 'SEND' | 'REQUEST' + type?: 'SEND' | 'REQUEST' | 'DEPOSIT' headerTitle?: string currencyAmount?: string isExternalWalletFlow?: boolean @@ -42,9 +61,14 @@ type DirectSuccessViewProps = { redirectTo?: string onComplete?: () => void points?: number + // props to receive data directly instead of from redux + chargeDetails?: TRequestChargeResponse | ChargeEntry | null + paymentDetails?: PaymentCreationResponse | null + parsedPaymentData?: ParsedURL | null + usdAmount?: string } -const DirectSuccessView = ({ +const PaymentSuccessView = ({ user, amount, message, @@ -57,10 +81,12 @@ const DirectSuccessView = ({ redirectTo = '/home', onComplete, points, + chargeDetails, + paymentDetails, + parsedPaymentData, + usdAmount, }: DirectSuccessViewProps) => { const router = useRouter() - const { chargeDetails, parsedPaymentData, usdAmount, paymentDetails } = usePaymentStore() - const dispatch = useDispatch() const { isDrawerOpen, selectedTransaction, openTransactionDetails, closeTransactionDetails } = useTransactionDetailsDrawer() const { user: authUser } = useUserStore() @@ -174,14 +200,14 @@ const DirectSuccessView = ({ }, [queryClient]) const handleDone = () => { - onComplete?.() + // Navigate first, then call onComplete - otherwise onComplete may reset state + // causing this component to unmount before router.push executes if (!!authUser?.user.userId) { - // reset payment state when done router.push('/home') - dispatch(paymentActions.resetPaymentState()) } else { router.push('/setup') } + onComplete?.() } const getTitle = () => { @@ -189,6 +215,7 @@ const DirectSuccessView = ({ if (isWithdrawFlow) return 'You just withdrew' if (type === 'SEND') return 'You sent ' if (type === 'REQUEST') return 'You requested ' + if (type === 'DEPOSIT') return 'You added ' } useEffect(() => { @@ -289,4 +316,4 @@ const DirectSuccessView = ({
) } -export default DirectSuccessView +export default PaymentSuccessView diff --git a/src/features/payments/shared/components/SendWithPeanutCta.tsx b/src/features/payments/shared/components/SendWithPeanutCta.tsx new file mode 100644 index 000000000..644937bc8 --- /dev/null +++ b/src/features/payments/shared/components/SendWithPeanutCta.tsx @@ -0,0 +1,115 @@ +'use client' + +/** + * primary cta button for peanut wallet payments + * + * shows different states: + * - not logged in: "continue with peanut" + redirects to signup, then redirects to the current page + * - logged in: "send with peanut" + executes payment + */ + +import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets' +import { Button, type ButtonProps } from '@/components/0_Bruddle/Button' +import type { IconName } from '@/components/Global/Icons/Icon' +import { useAuth } from '@/context/authContext' +import { saveRedirectUrl, saveToLocalStorage } from '@/utils/general.utils' +import Image from 'next/image' +import { useRouter } from 'next/navigation' +import { useMemo } from 'react' + +interface SendWithPeanutCtaProps extends ButtonProps { + title?: string + // when true, will redirect to login if user is not logged in + requiresAuth?: boolean + insufficientBalance?: boolean +} + +/** + * Button to continue with Peanut or login to continue with peanut icon + * @param title - The title of the button (optional) + * @param requiresAuth - Whether the button requires authentication + * @param onClick - The onClick handler + * @param props - The props for the button + * @returns The button component + */ + +export default function SendWithPeanutCta({ + title, + requiresAuth = true, + onClick, + insufficientBalance = false, + ...props +}: SendWithPeanutCtaProps) { + const router = useRouter() + const { user, isFetchingUser } = useAuth() + + const isLoggedIn = !!user?.user?.userId + + const handleClick = (e: React.MouseEvent) => { + // if auth is required and user is not logged in, redirect to login + if (requiresAuth && !user?.user?.userId && !isFetchingUser) { + saveRedirectUrl() + router.push('/setup') + return + } + + if (isLoggedIn && insufficientBalance) { + // save current url so back button works properly + saveRedirectUrl() + saveToLocalStorage('fromRequestFulfillment', 'true') + router.push('/add-money') + return + } + + // otherwise call the provided onClick handler + onClick?.(e) + } + + const icon = useMemo((): IconName | undefined => { + if (!isLoggedIn) { + return undefined + } + if (insufficientBalance) { + return 'arrow-down' + } + return 'arrow-up-right' + }, [isLoggedIn, insufficientBalance]) + + const peanutLogo = useMemo((): React.ReactNode => { + return ( +
+ Peanut Logo + Peanut Logo +
+ ) + }, []) + + return ( + + ) +} diff --git a/src/features/payments/shared/hooks/useChargeManager.ts b/src/features/payments/shared/hooks/useChargeManager.ts new file mode 100644 index 000000000..4d698db9f --- /dev/null +++ b/src/features/payments/shared/hooks/useChargeManager.ts @@ -0,0 +1,194 @@ +'use client' + +/** + * hook for managing charge lifecycle (create, fetch, cache) + * + * charges are payment requests stored in our backend. they track: + * - what token/chain the recipient wants + * - the amount requested + * - optional attachments/messages + * + * used by all payment flows before executing transactions + * + * @example + * const { createCharge, fetchCharge, charge } = useChargeManager() + * const newCharge = await createCharge({ tokenAmount: '10', ... }) + */ + +import { useState, useCallback } from 'react' +import { chargesApi } from '@/services/charges' +import { requestsApi } from '@/services/requests' +import { type TRequestChargeResponse, type TCharge, type TChargeTransactionType } from '@/services/services.types' +import { isNativeCurrency } from '@/utils/general.utils' +import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' +import { type Address } from 'viem' + +// params for creating a new charge +export interface CreateChargeParams { + tokenAmount: string + tokenAddress: Address + chainId: string + tokenSymbol: string + tokenDecimals: number + recipientAddress: Address + transactionType?: TChargeTransactionType + requestId?: string + reference?: string + attachment?: File + currencyAmount?: string + currencyCode?: string +} + +// return type for the hook +export interface UseChargeManagerReturn { + charge: TRequestChargeResponse | null + isCreating: boolean + isFetching: boolean + error: string | null + createCharge: (params: CreateChargeParams) => Promise + fetchCharge: (chargeId: string) => Promise + setCharge: (charge: TRequestChargeResponse | null) => void + reset: () => void +} + +export const useChargeManager = (): UseChargeManagerReturn => { + const [charge, setCharge] = useState(null) + const [isCreating, setIsCreating] = useState(false) + const [isFetching, setIsFetching] = useState(false) + const [error, setError] = useState(null) + + // fetch existing charge by id + const fetchCharge = useCallback(async (chargeId: string): Promise => { + setIsFetching(true) + setError(null) + + try { + const chargeDetails = await chargesApi.get(chargeId) + setCharge(chargeDetails) + return chargeDetails + } catch (err) { + const message = err instanceof Error ? err.message : 'failed to fetch charge' + setError(message) + throw err + } finally { + setIsFetching(false) + } + }, []) + + // create a new charge + const createCharge = useCallback(async (params: CreateChargeParams): Promise => { + setIsCreating(true) + setError(null) + + try { + // if requestId provided, validate it exists + let validRequestId = params.requestId + if (params.requestId) { + try { + const request = await requestsApi.get(params.requestId) + validRequestId = request.uuid + } catch { + throw new Error('invalid request id') + } + } + + // build the create charge payload + const localPrice = + params.currencyAmount && params.currencyCode + ? { amount: params.currencyAmount, currency: params.currencyCode } + : { amount: params.tokenAmount, currency: 'USD' } + + const createPayload: { + pricing_type: 'fixed_price' + local_price: { amount: string; currency: string } + baseUrl: string + requestId?: string + requestProps: { + chainId: string + tokenAmount: string + tokenAddress: Address + tokenType: peanutInterfaces.EPeanutLinkType + tokenSymbol: string + tokenDecimals: number + recipientAddress: Address + } + transactionType?: TChargeTransactionType + attachment?: File + reference?: string + mimeType?: string + filename?: string + } = { + pricing_type: 'fixed_price', + local_price: localPrice, + baseUrl: typeof window !== 'undefined' ? window.location.origin : '', + requestProps: { + chainId: params.chainId, + tokenAmount: params.tokenAmount, + tokenAddress: params.tokenAddress, + tokenType: isNativeCurrency(params.tokenAddress) + ? peanutInterfaces.EPeanutLinkType.native + : peanutInterfaces.EPeanutLinkType.erc20, + tokenSymbol: params.tokenSymbol, + tokenDecimals: params.tokenDecimals, + recipientAddress: params.recipientAddress, + }, + transactionType: params.transactionType, + } + + // add request id if provided + if (validRequestId) { + createPayload.requestId = validRequestId + } + + // add attachment if present + if (params.attachment) { + createPayload.attachment = params.attachment + createPayload.filename = params.attachment.name + createPayload.mimeType = params.attachment.type + } + + // add reference/message if present + if (params.reference) { + createPayload.reference = params.reference + } + + // create the charge + const chargeResponse: TCharge = await chargesApi.create(createPayload) + + if (!chargeResponse.data.id) { + throw new Error('charge created but missing uuid') + } + + // fetch full charge details + const chargeDetails = await chargesApi.get(chargeResponse.data.id) + setCharge(chargeDetails) + + return chargeDetails + } catch (err) { + const message = err instanceof Error ? err.message : 'failed to create charge' + setError(message) + throw err + } finally { + setIsCreating(false) + } + }, []) + + // reset all state + const reset = useCallback(() => { + setCharge(null) + setIsCreating(false) + setIsFetching(false) + setError(null) + }, []) + + return { + charge, + isCreating, + isFetching, + error, + createCharge, + fetchCharge, + setCharge, + reset, + } +} diff --git a/src/features/payments/shared/hooks/usePaymentRecorder.ts b/src/features/payments/shared/hooks/usePaymentRecorder.ts new file mode 100644 index 000000000..487fd6c1f --- /dev/null +++ b/src/features/payments/shared/hooks/usePaymentRecorder.ts @@ -0,0 +1,91 @@ +'use client' + +/** + * hook for recording payments to the backend after transaction execution + * + * after a blockchain transaction is confirmed, this hook notifies our backend + * so we can: + * - mark the charge as paid + * - update the recipient's balance/history + * - track cross-chain payment sources + * + * @example + * const { recordPayment } = usePaymentRecorder() + * await recordPayment({ chargeId, chainId, txHash, tokenAddress, payerAddress }) + */ + +import { useState, useCallback } from 'react' +import { chargesApi } from '@/services/charges' +import { type PaymentCreationResponse } from '@/services/services.types' +import { type Address } from 'viem' + +// params for recording a payment +export interface RecordPaymentParams { + chargeId: string + chainId: string + txHash: string + tokenAddress: Address + payerAddress: Address + // optional cross-chain source info + sourceChainId?: string + sourceTokenAddress?: string + sourceTokenSymbol?: string +} + +// return type for the hook +export interface UsePaymentRecorderReturn { + payment: PaymentCreationResponse | null + isRecording: boolean + error: string | null + recordPayment: (params: RecordPaymentParams) => Promise + reset: () => void +} + +export const usePaymentRecorder = (): UsePaymentRecorderReturn => { + const [payment, setPayment] = useState(null) + const [isRecording, setIsRecording] = useState(false) + const [error, setError] = useState(null) + + // record payment to backend + const recordPayment = useCallback(async (params: RecordPaymentParams): Promise => { + setIsRecording(true) + setError(null) + + try { + const paymentResponse = await chargesApi.createPayment({ + chargeId: params.chargeId, + chainId: params.chainId, + hash: params.txHash, + tokenAddress: params.tokenAddress, + payerAddress: params.payerAddress, + sourceChainId: params.sourceChainId, + sourceTokenAddress: params.sourceTokenAddress, + sourceTokenSymbol: params.sourceTokenSymbol, + }) + + setPayment(paymentResponse) + return paymentResponse + } catch (err) { + const message = err instanceof Error ? err.message : 'failed to record payment' + setError(message) + throw err + } finally { + setIsRecording(false) + } + }, []) + + // reset state + const reset = useCallback(() => { + setPayment(null) + setIsRecording(false) + setError(null) + }, []) + + return { + payment, + isRecording, + error, + recordPayment, + reset, + } +} diff --git a/src/features/payments/shared/hooks/useRouteCalculation.ts b/src/features/payments/shared/hooks/useRouteCalculation.ts new file mode 100644 index 000000000..2f9612671 --- /dev/null +++ b/src/features/payments/shared/hooks/useRouteCalculation.ts @@ -0,0 +1,233 @@ +'use client' + +/** + * hook for calculating cross-chain routes and preparing transactions + * + * handles two scenarios: + * 1. same chain + same token: prepares a simple transfer + * 2. cross-chain or different token: uses squid router to find best route + * todo: @dev squid to be updated in deposit v2 + * + * returns unsigned transactions ready to be sent via wallet + * + * @example + * const { calculateRoute, transactions, estimatedGasCostUsd } = useRouteCalculation() + * await calculateRoute({ source: { ... }, destination: { ... } }) + */ + +import { useState, useCallback } from 'react' +import { parseUnits } from 'viem' +import { type Address, type Hex } from 'viem' +import { peanut, interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' +import { getRoute, type PeanutCrossChainRoute } from '@/services/swap' +import { estimateTransactionCostUsd } from '@/app/actions/tokens' +import { areEvmAddressesEqual } from '@/utils/general.utils' +import { captureException } from '@sentry/nextjs' + +// source token info for route calculation +export interface RouteSourceInfo { + address: Address + tokenAddress: Address + chainId: string +} + +// destination charge info for route calculation +export interface RouteDestinationInfo { + recipientAddress: Address + tokenAddress: Address + tokenAmount: string + tokenDecimals: number + tokenType: number + chainId: string +} + +// unsigned transaction ready for execution +export interface PreparedTransaction { + to: Address + data?: Hex + value?: bigint +} + +// return type for the hook +export interface UseRouteCalculationReturn { + route: PeanutCrossChainRoute | null + transactions: PreparedTransaction[] | null + estimatedGasCostUsd: number | undefined + estimatedFromValue: string + slippagePercentage: number | undefined + isXChain: boolean + isDiffToken: boolean + isCalculating: boolean + isFeeEstimationError: boolean + error: string | null + calculateRoute: (params: { + source: RouteSourceInfo + destination: RouteDestinationInfo + usdAmount?: string + disableCoral?: boolean + skipGasEstimate?: boolean + }) => Promise + reset: () => void +} + +export const useRouteCalculation = (): UseRouteCalculationReturn => { + const [route, setRoute] = useState(null) + const [transactions, setTransactions] = useState(null) + const [estimatedGasCostUsd, setEstimatedGasCostUsd] = useState(undefined) + const [estimatedFromValue, setEstimatedFromValue] = useState('0') + const [slippagePercentage, setSlippagePercentage] = useState(undefined) + const [isCalculating, setIsCalculating] = useState(false) + const [isFeeEstimationError, setIsFeeEstimationError] = useState(false) + const [error, setError] = useState(null) + + // computed values + const [isXChain, setIsXChain] = useState(false) + const [isDiffToken, setIsDiffToken] = useState(false) + + // calculate route for cross-chain or same-chain swap + const calculateRoute = useCallback( + async ({ + source, + destination, + usdAmount, + disableCoral = false, + skipGasEstimate = false, + }: { + source: RouteSourceInfo + destination: RouteDestinationInfo + usdAmount?: string + disableCoral?: boolean + skipGasEstimate?: boolean + }) => { + setIsCalculating(true) + setError(null) + setIsFeeEstimationError(false) + setRoute(null) + setTransactions(null) + setEstimatedGasCostUsd(undefined) + + try { + const _isXChain = source.chainId !== destination.chainId + const _isDiffToken = !areEvmAddressesEqual(source.tokenAddress, destination.tokenAddress) + + setIsXChain(_isXChain) + setIsDiffToken(_isDiffToken) + + if (_isXChain || _isDiffToken) { + // cross-chain or token swap needed + const amount = usdAmount + ? { fromUsd: usdAmount } + : { toAmount: parseUnits(destination.tokenAmount, destination.tokenDecimals) } + + const xChainRoute = await getRoute( + { + from: source, + to: { + address: destination.recipientAddress, + tokenAddress: destination.tokenAddress, + chainId: destination.chainId, + }, + ...amount, + }, + { disableCoral } + ) + + if (xChainRoute.error) { + throw new Error(xChainRoute.error) + } + + const slippage = Number(xChainRoute.fromAmount) / Number(destination.tokenAmount) - 1 + + setRoute(xChainRoute) + setTransactions( + xChainRoute.transactions.map((tx) => ({ + to: tx.to, + data: tx.data, + value: BigInt(tx.value), + })) + ) + setEstimatedGasCostUsd(xChainRoute.feeCostsUsd) + setEstimatedFromValue(xChainRoute.fromAmount) + setSlippagePercentage(slippage) + } else { + // same chain, same token - prepare simple transfer + const tx = peanut.prepareRequestLinkFulfillmentTransaction({ + recipientAddress: destination.recipientAddress, + tokenAddress: destination.tokenAddress, + tokenAmount: destination.tokenAmount, + tokenDecimals: destination.tokenDecimals, + tokenType: destination.tokenType as peanutInterfaces.EPeanutLinkType, + }) + + if (!tx?.unsignedTx) { + throw new Error('failed to prepare transaction') + } + + const preparedTx: PreparedTransaction = { + to: tx.unsignedTx.to as Address, + data: tx.unsignedTx.data as Hex | undefined, + value: tx.unsignedTx.value ? BigInt(tx.unsignedTx.value.toString()) : undefined, + } + + setTransactions([preparedTx]) + setEstimatedFromValue(destination.tokenAmount) + setSlippagePercentage(undefined) + + // estimate gas for external wallets + if (!skipGasEstimate && tx.unsignedTx.from && tx.unsignedTx.to && tx.unsignedTx.data) { + try { + const gasCost = await estimateTransactionCostUsd( + tx.unsignedTx.from as Address, + tx.unsignedTx.to as Address, + tx.unsignedTx.data as Hex, + destination.chainId + ) + setEstimatedGasCostUsd(gasCost) + } catch (gasError) { + captureException(gasError) + setIsFeeEstimationError(true) + } + } else { + setEstimatedGasCostUsd(0) + } + } + } catch (err) { + const message = err instanceof Error ? err.message : 'failed to calculate route' + setError(message) + setIsFeeEstimationError(true) + } finally { + setIsCalculating(false) + } + }, + [] + ) + + // reset all state + const reset = useCallback(() => { + setRoute(null) + setTransactions(null) + setEstimatedGasCostUsd(undefined) + setEstimatedFromValue('0') + setSlippagePercentage(undefined) + setIsXChain(false) + setIsDiffToken(false) + setIsCalculating(false) + setIsFeeEstimationError(false) + setError(null) + }, []) + + return { + route, + transactions, + estimatedGasCostUsd, + estimatedFromValue, + slippagePercentage, + isXChain, + isDiffToken, + isCalculating, + isFeeEstimationError, + error, + calculateRoute, + reset, + } +} diff --git a/src/hooks/query/user.ts b/src/hooks/query/user.ts index 9f08f765c..70553493e 100644 --- a/src/hooks/query/user.ts +++ b/src/hooks/query/user.ts @@ -1,12 +1,12 @@ -import { USER } from '@/constants' import { type IUserProfile } from '@/interfaces' import { useAppDispatch, useUserStore } from '@/redux/hooks' import { userActions } from '@/redux/slices/user-slice' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { hitUserMetric } from '@/utils/metrics.utils' import { keepPreviousData, useQuery } from '@tanstack/react-query' import { usePWAStatus } from '../usePWAStatus' import { useDeviceType } from '../useGetDeviceType' +import { USER } from '@/constants/query.consts' export const useUserQuery = (dependsOn: boolean = true) => { const isPwa = usePWAStatus() diff --git a/src/hooks/useAccountSetup.ts b/src/hooks/useAccountSetup.ts index a086cc1de..c68e11af6 100644 --- a/src/hooks/useAccountSetup.ts +++ b/src/hooks/useAccountSetup.ts @@ -3,7 +3,8 @@ import { useRouter, useSearchParams } from 'next/navigation' import * as Sentry from '@sentry/nextjs' import { useAuth } from '@/context/authContext' import { WalletProviderType } from '@/interfaces' -import { getRedirectUrl, getValidRedirectUrl, clearRedirectUrl, clearAuthState } from '@/utils' +import { getRedirectUrl, getValidRedirectUrl, clearRedirectUrl } from '@/utils/general.utils' +import { clearAuthState } from '@/utils/auth.utils' import { POST_SIGNUP_ACTIONS } from '@/components/Global/PostSignupActionManager/post-signup-action.consts' import { useSetupStore } from '@/redux/hooks' @@ -20,6 +21,33 @@ export const useAccountSetup = () => { const [error, setError] = useState(null) const [isProcessing, setIsProcessing] = useState(false) + const handleRedirect = () => { + const redirect_uri = searchParams.get('redirect_uri') + if (redirect_uri) { + const validRedirectUrl = getValidRedirectUrl(redirect_uri, '/home') + console.log('[useAccountSetup] Redirecting to redirect_uri:', validRedirectUrl) + router.push(validRedirectUrl) + return true + } + + const localStorageRedirect = getRedirectUrl() + if (localStorageRedirect) { + const matchedAction = POST_SIGNUP_ACTIONS.find((action) => action.pathPattern.test(localStorageRedirect)) + if (matchedAction) { + console.log('[useAccountSetup] Matched post-signup action, redirecting to /home') + router.push('/home') + } else { + clearRedirectUrl() + const validRedirectUrl = getValidRedirectUrl(localStorageRedirect, '/home') + console.log('[useAccountSetup] Redirecting to localStorage redirect:', validRedirectUrl) + router.push(validRedirectUrl) + } + } else { + console.log('[useAccountSetup] No redirect found, going to /home') + router.push('/home') + } + } + /** * finalize account setup by adding account to db and navigating */ @@ -76,32 +104,7 @@ export const useAccountSetup = () => { } } - const redirect_uri = searchParams.get('redirect_uri') - if (redirect_uri) { - const validRedirectUrl = getValidRedirectUrl(redirect_uri, '/home') - console.log('[useAccountSetup] Redirecting to redirect_uri:', validRedirectUrl) - router.push(validRedirectUrl) - return true - } - - const localStorageRedirect = getRedirectUrl() - if (localStorageRedirect) { - const matchedAction = POST_SIGNUP_ACTIONS.find((action) => - action.pathPattern.test(localStorageRedirect) - ) - if (matchedAction) { - console.log('[useAccountSetup] Matched post-signup action, redirecting to /home') - router.push('/home') - } else { - clearRedirectUrl() - const validRedirectUrl = getValidRedirectUrl(localStorageRedirect, '/home') - console.log('[useAccountSetup] Redirecting to localStorage redirect:', validRedirectUrl) - router.push(validRedirectUrl) - } - } else { - console.log('[useAccountSetup] No redirect found, going to /home') - router.push('/home') - } + handleRedirect() return true } catch (e) { @@ -122,5 +125,6 @@ export const useAccountSetup = () => { isProcessing, error, setError, + handleRedirect, } } diff --git a/src/hooks/useAutoTruncatedAddress.ts b/src/hooks/useAutoTruncatedAddress.ts new file mode 100644 index 000000000..0c9156732 --- /dev/null +++ b/src/hooks/useAutoTruncatedAddress.ts @@ -0,0 +1,199 @@ +import { useEffect, useState, useRef, useCallback } from 'react' + +/** Safety margin multiplier to account for font rendering differences between canvas and DOM */ +const SAFETY_MARGIN = 0.9 + +/** + * Calculates the optimal truncation for an address to fit within a container width. + * Returns the formatted address like "0xF3...3he7e" with dynamic character counts. + * + * Shows as many characters as fit in the container, never causing overflow. + * + * @param address - The full address to truncate + * @param containerWidth - Available width in pixels + * @param charWidth - Estimated width per character in pixels (default: 8) + * @returns Formatted address string + */ +export const formatAddressToFitWidth = (address: string, containerWidth: number, charWidth: number = 8): string => { + if (!address || containerWidth <= 0) return address || '' + + // Apply safety margin to container width + const safeWidth = containerWidth * SAFETY_MARGIN + + // "..." is 3 characters + const ellipsisWidth = charWidth * 3 + + // Calculate max characters that can fit (excluding ellipsis) + const availableWidth = safeWidth - ellipsisWidth + const maxChars = Math.floor(availableWidth / charWidth) + + // If full address fits (with margin), return it + if (address.length * charWidth <= safeWidth) return address + + // Calculate chars for each side (half on each side) + const fitsPerSide = Math.floor(maxChars / 2) + const charsPerSide = Math.max(1, fitsPerSide) + + // Edge case: container too narrow for even minimal truncation (need at least 1+3+1=5 chars width) + if (maxChars < 2) { + return address.substring(0, Math.max(1, maxChars)) + 'โ€ฆ' + } + + const firstBit = address.substring(0, charsPerSide) + const lastBit = address.substring(address.length - charsPerSide) + + return `${firstBit}...${lastBit}` +} + +// Cache for measured char widths to avoid repeated DOM operations +const charWidthCache = new WeakMap() + +/** + * Measures the approximate character width for a given element's font. + * Results are cached per element to avoid layout thrashing. + */ +const measureCharWidth = (element: HTMLElement): number => { + const computedStyle = window.getComputedStyle(element) + const fontKey = `${computedStyle.fontSize}-${computedStyle.fontFamily}-${computedStyle.fontWeight}` + + // Check cache + const cached = charWidthCache.get(element) + if (cached && cached.fontKey === fontKey) { + return cached.width + } + + // Measure using canvas (no DOM manipulation, much faster) + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + if (!ctx) return 8 // Fallback + + ctx.font = `${computedStyle.fontWeight} ${computedStyle.fontSize} ${computedStyle.fontFamily}` + const testString = '0123456789abcdefx' + const width = ctx.measureText(testString).width / testString.length + + // Cache result + charWidthCache.set(element, { width, fontKey }) + + return width +} + +interface UseAutoTruncatedAddressOptions { + /** @deprecated No longer used - kept for API compatibility */ + minChars?: number + /** Extra padding to subtract from container width (default: 0) */ + padding?: number +} + +/** + * React hook that automatically truncates an address to fit within a container. + * Uses ResizeObserver to adapt to container size changes. + * + * @example + * ```tsx + * const { containerRef, truncatedAddress } = useAutoTruncatedAddress(depositAddress) + * + * return ( + *
+ * {truncatedAddress} + *
+ * ) + * ``` + */ +export const useAutoTruncatedAddress = ( + address: string, + options: UseAutoTruncatedAddressOptions = {} +): { + containerRef: (node: T | null) => void + truncatedAddress: string +} => { + const { padding = 0 } = options + const elementRef = useRef(null) + const [truncatedAddress, setTruncatedAddress] = useState(address) + const resizeObserverRef = useRef(null) + const rafIdRef = useRef(null) + const lastWidthRef = useRef(0) + + // Store options in refs to avoid recreating callbacks + const optionsRef = useRef({ padding, address }) + optionsRef.current = { padding, address } + + const updateTruncation = useCallback(() => { + const container = elementRef.current + const { address: addr, padding: pad } = optionsRef.current + + if (!container || !addr) { + setTruncatedAddress(addr || '') + return + } + + const containerWidth = container.offsetWidth - pad + + // Skip if width hasn't changed (avoid unnecessary work) + if (containerWidth === lastWidthRef.current && truncatedAddress !== addr) { + return + } + lastWidthRef.current = containerWidth + + const charWidth = measureCharWidth(container) + const formatted = formatAddressToFitWidth(addr, containerWidth, charWidth) + + setTruncatedAddress(formatted) + }, []) // Empty deps - uses refs for values + + // Debounced update using requestAnimationFrame + const scheduleUpdate = useCallback(() => { + if (rafIdRef.current) { + cancelAnimationFrame(rafIdRef.current) + } + rafIdRef.current = requestAnimationFrame(() => { + updateTruncation() + rafIdRef.current = null + }) + }, [updateTruncation]) + + // Stable callback ref + const containerRef = useCallback( + (node: T | null) => { + // Cleanup previous observer + if (resizeObserverRef.current) { + resizeObserverRef.current.disconnect() + resizeObserverRef.current = null + } + + elementRef.current = node + + if (node) { + // Initial calculation (immediate, no debounce) + updateTruncation() + + // Watch for container size changes (debounced) + resizeObserverRef.current = new ResizeObserver(scheduleUpdate) + resizeObserverRef.current.observe(node) + } + }, + [updateTruncation, scheduleUpdate] + ) + + // Update when address or padding changes + useEffect(() => { + lastWidthRef.current = 0 // Reset to force recalculation + updateTruncation() + }, [address, padding, updateTruncation]) + + // Cleanup on unmount + useEffect(() => { + return () => { + if (resizeObserverRef.current) { + resizeObserverRef.current.disconnect() + } + if (rafIdRef.current) { + cancelAnimationFrame(rafIdRef.current) + } + } + }, []) + + return { + containerRef, + truncatedAddress, + } +} diff --git a/src/hooks/useBravePWAInstallState.ts b/src/hooks/useBravePWAInstallState.ts new file mode 100644 index 000000000..72ce6a4ce --- /dev/null +++ b/src/hooks/useBravePWAInstallState.ts @@ -0,0 +1,67 @@ +'use client' + +import { useEffect, useState } from 'react' +import { usePWAStatus } from './usePWAStatus' +import { BrowserType, useGetBrowserType } from './useGetBrowserType' + +type InstalledRelatedApp = { platform: string; url?: string; id?: string; version?: string } + +/** + * tracks whether the user is on Brave and has the Peanut PWA installed. + * + * combines: + * - current PWA display mode (standalone/web) + * - `navigator.getInstalledRelatedApps` (when available) + * - `appinstalled` event + */ +export const useBravePWAInstallState = () => { + const isStandalonePWA = usePWAStatus() + const { browserType } = useGetBrowserType() + const [hasInstalledRelatedApp, setHasInstalledRelatedApp] = useState(false) + + useEffect(() => { + if (typeof window === 'undefined') return + + const _navigator = window.navigator as Navigator & { + getInstalledRelatedApps?: () => Promise + } + + if (typeof _navigator.getInstalledRelatedApps !== 'function') return + + let cancelled = false + + const checkInstallation = async () => { + try { + const installedApps = await _navigator.getInstalledRelatedApps!() + if (!cancelled) { + setHasInstalledRelatedApp(installedApps.length > 0) + } + } catch { + if (!cancelled) { + setHasInstalledRelatedApp(false) + } + } + } + + void checkInstallation() + + const handleAppInstalled = () => { + if (!cancelled) { + setHasInstalledRelatedApp(true) + } + } + + window.addEventListener('appinstalled', handleAppInstalled) + + return () => { + cancelled = true + window.removeEventListener('appinstalled', handleAppInstalled) + } + }, []) + + const isPWAInstalled = isStandalonePWA || hasInstalledRelatedApp + const isBrave = browserType === BrowserType.BRAVE + const isBravePWAInstalled = isBrave && isPWAInstalled + + return { isBrave, isPWAInstalled, isBravePWAInstalled } +} diff --git a/src/hooks/useBridgeKycFlow.ts b/src/hooks/useBridgeKycFlow.ts index adbb746cc..2cde6dead 100644 --- a/src/hooks/useBridgeKycFlow.ts +++ b/src/hooks/useBridgeKycFlow.ts @@ -3,7 +3,7 @@ import { useRouter } from 'next/navigation' import { type IFrameWrapperProps } from '@/components/Global/IframeWrapper' import { useWebSocket } from '@/hooks/useWebSocket' import { useUserStore } from '@/redux/hooks' -import { type BridgeKycStatus, convertPersonaUrl } from '@/utils' +import { type BridgeKycStatus, convertPersonaUrl } from '@/utils/bridge-accounts.utils' import { type InitiateKycResponse } from '@/app/actions/types/users.types' import { getKycDetails, updateUserById } from '@/app/actions/users' import { type IUserKycVerification } from '@/interfaces' diff --git a/src/hooks/useContacts.ts b/src/hooks/useContacts.ts index 84ee7074a..9f8b70b55 100644 --- a/src/hooks/useContacts.ts +++ b/src/hooks/useContacts.ts @@ -8,21 +8,24 @@ export type { Contact } interface UseContactsOptions { limit?: number + search?: string } /** - * hook to fetch all contacts for the current user with infinite scroll + * hook to fetch all contacts for the current user with infinite scroll and optional search * includes: inviter, invitees, and all transaction counterparties (sent/received money, request pots) + * when search is provided, filters contacts by username or full name on the server */ export function useContacts(options: UseContactsOptions = {}) { - const { limit = 50 } = options + const { limit = 50, search } = options const { data, isLoading, error, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = useInfiniteQuery({ - queryKey: [CONTACTS, limit], + queryKey: [CONTACTS, limit, search], queryFn: async ({ pageParam = 0 }): Promise => { const result = await getContacts({ limit, offset: pageParam * limit, + search, }) if (result.error) { diff --git a/src/hooks/useGetBrowserType.ts b/src/hooks/useGetBrowserType.ts index 7b2e29c8f..2eadba972 100644 --- a/src/hooks/useGetBrowserType.ts +++ b/src/hooks/useGetBrowserType.ts @@ -53,17 +53,17 @@ export const useGetBrowserType = () => { return BrowserType.SAMSUNG } + // Check for Brave browser BEFORE Chrome (Brave uses Chrome UA so must check first) + if ((navigator as any).brave && (await (navigator as any).brave.isBrave?.()) === true) { + return BrowserType.BRAVE + } + // Check for Chrome (desktop and mobile) // CriOS = Chrome on iOS if (userAgent.includes('chrome') || userAgent.includes('crios')) { return BrowserType.CHROME } - // Check for Brave browser (uses Chrome UA) - if ((navigator as any).brave && (await (navigator as any).brave.isBrave?.()) === true) { - return BrowserType.BRAVE - } - // Check for Safari (desktop and mobile - must be last since all iOS browsers include "safari" in UA) if (userAgent.includes('safari')) { return BrowserType.SAFARI diff --git a/src/hooks/useHoldToClaim.ts b/src/hooks/useHoldToClaim.ts index 4618547ef..27acdb961 100644 --- a/src/hooks/useHoldToClaim.ts +++ b/src/hooks/useHoldToClaim.ts @@ -1,5 +1,5 @@ +import { PERK_HOLD_DURATION_MS } from '@/constants/general.consts' import { useCallback, useEffect, useRef, useState } from 'react' -import { PERK_HOLD_DURATION_MS } from '@/constants' export type ShakeIntensity = 'none' | 'weak' | 'medium' | 'strong' | 'intense' diff --git a/src/hooks/useHomeCarouselCTAs.tsx b/src/hooks/useHomeCarouselCTAs.tsx index 5cc35372b..70a43822c 100644 --- a/src/hooks/useHomeCarouselCTAs.tsx +++ b/src/hooks/useHomeCarouselCTAs.tsx @@ -7,14 +7,11 @@ import { useNotifications } from './useNotifications' import { useRouter } from 'next/navigation' import useKycStatus from './useKycStatus' import type { StaticImageData } from 'next/image' -import { useQrCodeContext } from '@/context/QrCodeContext' -import { getUserPreferences, updateUserPreferences } from '@/utils' -import { DEVCONNECT_LOGO, STAR_STRAIGHT_ICON } from '@/assets' -import { DEVCONNECT_INTENT_EXPIRY_MS } from '@/constants' +import { useModalsContext } from '@/context/ModalsContext' import { DeviceType, useDeviceType } from './useGetDeviceType' import { usePWAStatus } from './usePWAStatus' -import { useModalsContext } from '@/context/ModalsContext' import { useGeoLocation } from './useGeoLocation' +import { STAR_STRAIGHT_ICON } from '@/assets' export type CarouselCTA = { id: string @@ -42,54 +39,9 @@ export const useHomeCarouselCTAs = () => { const isPwa = usePWAStatus() const { setIsIosPwaInstallModalOpen } = useModalsContext() - const { setIsQRScannerOpen } = useQrCodeContext() + const { setIsQRScannerOpen } = useModalsContext() const { countryCode: userCountryCode } = useGeoLocation() - // -------------------------------------------------------------------------------------------------- - /** - * check if there's a pending devconnect intent and clean up old ones - * - * @dev: note, this code needs to be deleted post devconnect, this is just to temporarily support onramp to devconnect wallet using bank accounts - */ - const [pendingDevConnectIntent, setPendingDevConnectIntent] = useState< - | { - id: string - recipientAddress: string - chain: string - amount: string - onrampId?: string - createdAt: number - status: 'pending' | 'completed' - } - | undefined - >(undefined) - - useEffect(() => { - if (!user?.user?.userId) { - setPendingDevConnectIntent(undefined) - return - } - - const prefs = getUserPreferences(user.user.userId) - const intents = prefs?.devConnectIntents ?? [] - - // clean up intents older than 7 days - const expiryTime = Date.now() - DEVCONNECT_INTENT_EXPIRY_MS - const recentIntents = intents.filter((intent) => intent.createdAt >= expiryTime && intent.status === 'pending') - - // update user preferences if we cleaned up any old intents - if (recentIntents.length !== intents.length) { - updateUserPreferences(user.user.userId, { - devConnectIntents: recentIntents, - }) - } - - // get the most recent pending intent (sorted by createdAt descending) - const mostRecentIntent = recentIntents.sort((a, b) => b.createdAt - a.createdAt)[0] - setPendingDevConnectIntent(mostRecentIntent) - }, [user?.user?.userId]) - // -------------------------------------------------------------------------------------------------- - const generateCarouselCTAs = useCallback(() => { const _carouselCTAs: CarouselCTA[] = [] @@ -111,7 +63,6 @@ export const useHomeCarouselCTAs = () => { }, }) } - // show notification cta only in pwa when notifications are not granted // clicking it triggers native prompt (or shows reinstall modal if denied) if (!isPermissionGranted && isPwa) { @@ -190,34 +141,6 @@ export const useHomeCarouselCTAs = () => { iconSize: 16, }) } - // ------------------------------------------------------------------------------------------------ - - // ------------------------------------------------------------------------------------------------ - // add devconnect payment cta if there's a pending intent - // @dev: note, this code needs to be deleted post devconnect, this is just to temporarily support onramp to devconnect wallet using bank accounts - if (pendingDevConnectIntent) { - _carouselCTAs.push({ - id: 'devconnect-payment', - title: 'Fund your DevConnect wallet', - description: `Deposit funds to your DevConnect wallet`, - logo: DEVCONNECT_LOGO, - icon: 'arrow-up-right', - onClick: () => { - // navigate to the semantic request flow where user can pay with peanut wallet - const paymentUrl = `/${pendingDevConnectIntent.recipientAddress}@${pendingDevConnectIntent.chain}` - router.push(paymentUrl) - }, - onClose: () => { - // remove the intent when user dismisses the cta - if (user?.user?.userId) { - updateUserPreferences(user.user.userId, { - devConnectIntents: [], - }) - } - }, - }) - } - // -------------------------------------------------------------------------------------------------- if (!hasKycApproval && !isUserBridgeKycUnderReview) { _carouselCTAs.push({ @@ -242,7 +165,6 @@ export const useHomeCarouselCTAs = () => { setCarouselCTAs(_carouselCTAs) }, [ - pendingDevConnectIntent, user?.user?.userId, isPermissionGranted, isPermissionDenied, diff --git a/src/hooks/useLogin.tsx b/src/hooks/useLogin.tsx index 1c4c66183..57f96abc5 100644 --- a/src/hooks/useLogin.tsx +++ b/src/hooks/useLogin.tsx @@ -1,7 +1,7 @@ import { useAuth } from '@/context/authContext' import { useZeroDev } from './useZeroDev' import { useEffect, useState } from 'react' -import { getRedirectUrl, getValidRedirectUrl, clearRedirectUrl } from '@/utils' +import { getRedirectUrl, getValidRedirectUrl, clearRedirectUrl } from '@/utils/general.utils' import { useRouter, useSearchParams } from 'next/navigation' /** diff --git a/src/hooks/useMantecaKycFlow.ts b/src/hooks/useMantecaKycFlow.ts index ff57a2c82..b0f0864ab 100644 --- a/src/hooks/useMantecaKycFlow.ts +++ b/src/hooks/useMantecaKycFlow.ts @@ -3,9 +3,9 @@ import type { IFrameWrapperProps } from '@/components/Global/IframeWrapper' import { mantecaApi } from '@/services/manteca' import { useAuth } from '@/context/authContext' import { type CountryData, MantecaSupportedExchanges } from '@/components/AddMoney/consts' -import { BASE_URL } from '@/constants' import { MantecaKycStatus } from '@/interfaces' import { useWebSocket } from './useWebSocket' +import { BASE_URL } from '@/constants/general.consts' type UseMantecaKycFlowOptions = { onClose?: () => void diff --git a/src/hooks/useNonEurSepaRedirect.ts b/src/hooks/useNonEurSepaRedirect.ts deleted file mode 100644 index cf4d13395..000000000 --- a/src/hooks/useNonEurSepaRedirect.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { useEffect, useMemo } from 'react' -import { useRouter } from 'next/navigation' -import { - countryData, - NON_EUR_SEPA_ALPHA2, - ALL_COUNTRIES_ALPHA3_TO_ALPHA2, - type CountryData, -} from '@/components/AddMoney/consts' -import countryCurrencyMappings from '@/constants/countryCurrencyMapping' - -interface UseNonEurSepaRedirectOptions { - countryIdentifier?: string // the country path or code (e.g., 'united-kingdom' or 'GB') - redirectPath?: string // redirect path on failure (e.g., '/add-money' or '/withdraw') - shouldRedirect?: boolean // whether to actually redirect or just return the status -} - -interface UseNonEurSepaRedirectResult { - isBlocked: boolean // true if the country is a non-eur sepa country and bank operations should be blocked - country: CountryData | null // the detected country object -} - -/** - * hook to check if a country is a non-eur sepa country and optionally redirect - * non-eur sepa countries are those that use sepa but don't have eur as their currency - * (e.g., poland with pln, hungary with huf, etc.) - */ -export function useNonEurSepaRedirect({ - countryIdentifier, - redirectPath, - shouldRedirect = true, -}: UseNonEurSepaRedirectOptions = {}): UseNonEurSepaRedirectResult { - const router = useRouter() - - // find the country from the identifier (could be path, iso2, or iso3) - const country = useMemo(() => { - if (!countryIdentifier) return null - - // try to find by path first - let found = countryData.find((c) => c.type === 'country' && c.path === countryIdentifier.toLowerCase()) - - // if not found, try by iso2 - if (!found) { - found = countryData.find( - (c) => c.type === 'country' && c.iso2?.toLowerCase() === countryIdentifier.toLowerCase() - ) - } - - // if not found, try by iso3 - if (!found) { - found = countryData.find( - (c) => c.type === 'country' && c.iso3?.toLowerCase() === countryIdentifier.toLowerCase() - ) - } - - return found || null - }, [countryIdentifier]) - - // determine if the country should be blocked for bank operations - const isBlocked = useMemo(() => { - if (!country || country.type !== 'country') return false - - // get the 2-letter country code for the check - let countryCode: string | undefined - - // try to get from iso2 first - if (country.iso2) { - countryCode = country.iso2 - } - // otherwise try to map from iso3 - else if (country.iso3 && ALL_COUNTRIES_ALPHA3_TO_ALPHA2[country.iso3]) { - countryCode = ALL_COUNTRIES_ALPHA3_TO_ALPHA2[country.iso3] - } - // fallback to id - else { - countryCode = country.id - } - - if (!countryCode) return false - - // check if it's in the non-eur sepa set - if (NON_EUR_SEPA_ALPHA2.has(countryCode)) { - return true - } - - // additional check using currency mappings - // this catches countries where currency is not usd/eur/mxn - const currencyMapping = countryCurrencyMappings.find( - (currency) => - countryCode?.toLowerCase() === currency.country.toLowerCase() || - currency.path?.toLowerCase() === country.path?.toLowerCase() - ) - - const isNonStandardCurrency = !!( - currencyMapping && - currencyMapping.currencyCode && - currencyMapping.currencyCode !== 'EUR' && - currencyMapping.currencyCode !== 'USD' && - currencyMapping.currencyCode !== 'MXN' - ) - - return isNonStandardCurrency - }, [country]) - - // redirect if needed - useEffect(() => { - if (isBlocked && shouldRedirect && redirectPath) { - router.replace(redirectPath) - } - }, [isBlocked, shouldRedirect, redirectPath, router]) - - return { - isBlocked, - country, - } -} diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts index 3fdd84bbd..6388138b0 100644 --- a/src/hooks/useNotifications.ts +++ b/src/hooks/useNotifications.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import OneSignal from 'react-onesignal' -import { getUserPreferences, updateUserPreferences } from '@/utils' +import { getUserPreferences, updateUserPreferences } from '@/utils/general.utils' import { useUserStore } from '@/redux/hooks' export function useNotifications() { diff --git a/src/hooks/usePaymentInitiator.ts b/src/hooks/usePaymentInitiator.ts deleted file mode 100644 index 528494636..000000000 --- a/src/hooks/usePaymentInitiator.ts +++ /dev/null @@ -1,809 +0,0 @@ -import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants' -import { tokenSelectorContext } from '@/context' -import { useWallet } from '@/hooks/wallet/useWallet' -import { type ParsedURL } from '@/lib/url-parser/types/payment' -import { useAppDispatch, usePaymentStore } from '@/redux/hooks' -import { paymentActions } from '@/redux/slices/payment-slice' -import { type IAttachmentOptions } from '@/redux/types/send-flow.types' -import { chargesApi } from '@/services/charges' -import { requestsApi } from '@/services/requests' -import { - type CreateChargeRequest, - type PaymentCreationResponse, - type TCharge, - type TChargeTransactionType, - type TRequestChargeResponse, -} from '@/services/services.types' -import { areEvmAddressesEqual, ErrorHandler, isNativeCurrency, isTxReverted } from '@/utils' -import { useAppKitAccount } from '@reown/appkit/react' -import { peanut, interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' -import { useCallback, useContext, useEffect, useMemo, useState } from 'react' -import { parseUnits } from 'viem' -import type { TransactionReceipt, Address, Hex, Hash } from 'viem' -import { useConfig, useSendTransaction, useSwitchChain, useAccount as useWagmiAccount } from 'wagmi' -import { waitForTransactionReceipt } from 'wagmi/actions' -import { getRoute, type PeanutCrossChainRoute } from '@/services/swap' -import { estimateTransactionCostUsd } from '@/app/actions/tokens' -import { captureException } from '@sentry/nextjs' -import { useRouter } from 'next/navigation' - -enum ELoadingStep { - IDLE = 'Idle', - PREPARING_TRANSACTION = 'Preparing Transaction', - SENDING_TRANSACTION = 'Sending Transaction', - CONFIRMING_TRANSACTION = 'Confirming Transaction', - UPDATING_PAYMENT_STATUS = 'Updating Payment Status', - CHARGE_CREATED = 'Charge Created', - ERROR = 'Error', - SUCCESS = 'Success', - FETCHING_CHARGE_DETAILS = 'Fetching Charge Details', - CREATING_CHARGE = 'Creating Charge', - SWITCHING_NETWORK = 'Switching Network', -} - -type LoadingStep = `${ELoadingStep}` - -export interface InitiatePaymentPayload { - recipient: ParsedURL['recipient'] - tokenAmount: string - chargeId?: string - skipChargeCreation?: boolean - requestId?: string // optional request ID from URL - currency?: { - code: string - symbol: string - price: number - } - currencyAmount?: string - isExternalWalletFlow?: boolean - transactionType?: TChargeTransactionType - attachmentOptions?: IAttachmentOptions - returnAfterChargeCreation?: boolean -} - -interface InitiationResult { - status: string - charge?: TRequestChargeResponse | null - payment?: PaymentCreationResponse | null - error?: string | null - txHash?: string | null - success?: boolean -} - -// hook for handling payment initiation and processing -export const usePaymentInitiator = () => { - const dispatch = useAppDispatch() - const { requestDetails, chargeDetails: chargeDetailsFromStore, currentView } = usePaymentStore() - const { selectedTokenData, selectedChainID, selectedTokenAddress, setIsXChain } = useContext(tokenSelectorContext) - const { isConnected: isPeanutWallet, address: peanutWalletAddress, sendTransactions, sendMoney } = useWallet() - const { switchChainAsync } = useSwitchChain() - const { address: wagmiAddress } = useAppKitAccount() - const { sendTransactionAsync } = useSendTransaction() - const router = useRouter() - const config = useConfig() - const { chain: connectedWalletChain } = useWagmiAccount() - - const [slippagePercentage, setSlippagePercentage] = useState(undefined) - const [unsignedTx, setUnsignedTx] = useState(null) - const [xChainUnsignedTxs, setXChainUnsignedTxs] = useState( - null - ) - const [isFeeEstimationError, setIsFeeEstimationError] = useState(false) - - const [isCalculatingFees, setIsCalculatingFees] = useState(false) - const [isPreparingTx, setIsPreparingTx] = useState(false) - - const [estimatedGasCostUsd, setEstimatedGasCostUsd] = useState(undefined) - const [estimatedFromValue, setEstimatedFromValue] = useState('0') - const [loadingStep, setLoadingStep] = useState('Idle') - const [error, setError] = useState(null) - const [createdChargeDetails, setCreatedChargeDetails] = useState(null) - const [transactionHash, setTransactionHash] = useState(null) - const [paymentDetails, setPaymentDetails] = useState(null) - const [isEstimatingGas, setIsEstimatingGas] = useState(false) - const [xChainRoute, setXChainRoute] = useState(undefined) - - // use chargeDetails from the store primarily, fallback to createdChargeDetails - const activeChargeDetails = useMemo( - () => chargeDetailsFromStore ?? createdChargeDetails, - [chargeDetailsFromStore, createdChargeDetails] - ) - - const isXChain = useMemo(() => { - if (!activeChargeDetails || !selectedChainID) return false - return selectedChainID !== activeChargeDetails.chainId - }, [activeChargeDetails, selectedChainID]) - - const diffTokens = useMemo(() => { - if (!selectedTokenData || !activeChargeDetails) return false - return !areEvmAddressesEqual(selectedTokenData.address, activeChargeDetails.tokenAddress) - }, [selectedTokenData, activeChargeDetails]) - - const isProcessing = useMemo( - () => - loadingStep !== 'Idle' && - loadingStep !== 'Success' && - loadingStep !== 'Error' && - loadingStep !== 'Charge Created', - [loadingStep] - ) - const reset = useCallback(() => { - setError(null) - setLoadingStep('Idle') - setIsFeeEstimationError(false) - setIsCalculatingFees(false) - setIsPreparingTx(false) - setIsEstimatingGas(false) - setUnsignedTx(null) - setXChainUnsignedTxs(null) - setXChainRoute(undefined) - setEstimatedFromValue('0') - setSlippagePercentage(undefined) - setEstimatedGasCostUsd(undefined) - setTransactionHash(null) - setPaymentDetails(null) - setCreatedChargeDetails(null) - }, []) - - // reset state - useEffect(() => { - reset() - }, [selectedChainID, selectedTokenAddress, requestDetails, reset]) - - const handleError = useCallback( - (err: unknown, step: string): InitiationResult => { - console.error(`Error during ${step}:`, err) - const errorMessage = ErrorHandler(err) - setError(errorMessage) - setLoadingStep('Error') - if (activeChargeDetails && step !== 'Creating Charge') { - const currentUrl = new URL(window.location.href) - if (currentUrl.searchParams.get('chargeId') === activeChargeDetails.uuid) { - const newUrl = new URL(window.location.href) - newUrl.searchParams.delete('chargeId') - // Use router.push (not window.history.replaceState) so that - // the components using the search params will be updated - router.push(newUrl.pathname + newUrl.search) - } - } - return { - status: 'Error', - error: errorMessage, - charge: activeChargeDetails, - success: false, - } - }, - [activeChargeDetails, router] - ) - - // prepare transaction details (called from Confirm view) - const prepareTransactionDetails = useCallback( - async ({ - chargeDetails, - from, - usdAmount, - disableCoral = false, - }: { - chargeDetails: TRequestChargeResponse - from: { - address: Address - tokenAddress: Address - chainId: string - } - usdAmount?: string - disableCoral?: boolean - }) => { - setError(null) - setIsFeeEstimationError(false) - setUnsignedTx(null) - setXChainUnsignedTxs(null) - setXChainRoute(undefined) - - setEstimatedGasCostUsd(undefined) - - setIsPreparingTx(true) - - try { - const _isXChain = from.chainId !== chargeDetails.chainId - const _diffTokens = !areEvmAddressesEqual(from.tokenAddress, chargeDetails.tokenAddress) - setIsXChain(_isXChain) - - if (_isXChain || _diffTokens) { - setLoadingStep('Preparing Transaction') - setIsCalculatingFees(true) - const amount = usdAmount - ? { - fromUsd: usdAmount, - } - : { - toAmount: parseUnits(chargeDetails.tokenAmount, chargeDetails.tokenDecimals), - } - const xChainRoute = await getRoute( - { - from, - to: { - address: chargeDetails.requestLink.recipientAddress as Address, - tokenAddress: chargeDetails.tokenAddress as Address, - chainId: chargeDetails.chainId, - }, - ...amount, - }, - { disableCoral } - ) - - if (xChainRoute.error) throw new Error(xChainRoute.error) - - const slippagePercentage = Number(xChainRoute.fromAmount) / Number(chargeDetails.tokenAmount) - 1 - setXChainRoute(xChainRoute) - setXChainUnsignedTxs( - xChainRoute.transactions.map((tx) => ({ - to: tx.to, - data: tx.data, - value: BigInt(tx.value), - })) - ) - setIsCalculatingFees(false) - setEstimatedGasCostUsd(xChainRoute.feeCostsUsd) - setEstimatedFromValue(xChainRoute.fromAmount) - setSlippagePercentage(slippagePercentage) - } else { - setLoadingStep('Preparing Transaction') - const tx = peanut.prepareRequestLinkFulfillmentTransaction({ - recipientAddress: chargeDetails.requestLink.recipientAddress, - tokenAddress: chargeDetails.tokenAddress, - tokenAmount: chargeDetails.tokenAmount, - tokenDecimals: chargeDetails.tokenDecimals, - tokenType: Number(chargeDetails.tokenType) as peanutInterfaces.EPeanutLinkType, - }) - - if (!tx?.unsignedTx) { - throw new Error('Failed to prepare transaction') - } - - setIsCalculatingFees(true) - let gasCost = 0 - if (!isPeanutWallet) { - try { - gasCost = await estimateTransactionCostUsd( - tx.unsignedTx.from! as Address, - tx.unsignedTx.to! as Address, - tx.unsignedTx.data! as Hex, - chargeDetails.chainId - ) - } catch (error) { - captureException(error) - setIsFeeEstimationError(true) - } - } - setEstimatedGasCostUsd(gasCost) - setIsCalculatingFees(false) - setUnsignedTx(tx.unsignedTx) - setEstimatedFromValue(chargeDetails.tokenAmount) - setSlippagePercentage(undefined) - } - setLoadingStep('Idle') - } catch (err) { - console.error('Error preparing transaction details:', err) - const errorMessage = ErrorHandler(err) - setError(errorMessage) - setIsFeeEstimationError(true) - setLoadingStep('Error') - } finally { - setIsPreparingTx(false) - } - }, - [setIsXChain, isPeanutWallet] - ) - - // helper function: determine charge details (fetch or create) - const determineChargeDetails = useCallback( - async ( - payload: InitiatePaymentPayload - ): Promise<{ chargeDetails: TRequestChargeResponse; chargeCreated: boolean }> => { - let chargeDetailsToUse: TRequestChargeResponse | null = null - let chargeCreated = false - - if (payload.chargeId) { - chargeDetailsToUse = activeChargeDetails - if (!chargeDetailsToUse || chargeDetailsToUse.uuid !== payload.chargeId) { - setLoadingStep('Fetching Charge Details') - chargeDetailsToUse = await chargesApi.get(payload.chargeId) - setCreatedChargeDetails(chargeDetailsToUse) - } - } else { - setLoadingStep('Creating Charge') - let validRequestId: string | undefined = payload.requestId - - if (payload.requestId) { - try { - const request = await requestsApi.get(payload.requestId) - validRequestId = request.uuid - } catch (error) { - console.error('Invalid request ID provided:', payload.requestId, error) - throw new Error('Invalid request ID') - } - } else if (!requestDetails) { - console.error('Request details not found and cannot create new one for this payment type.') - throw new Error('Request details not found.') - } else { - validRequestId = requestDetails.uuid - } - - if (!validRequestId) { - console.error('Could not determine request ID for charge creation.') - throw new Error('Could not determine request ID') - } - - const recipientChainId = requestDetails?.chainId ?? selectedChainID - const recipientTokenAddress = requestDetails?.tokenAddress ?? selectedTokenAddress - const recipientTokenSymbol = requestDetails?.tokenSymbol ?? selectedTokenData?.symbol ?? 'TOKEN' - const recipientTokenDecimals = requestDetails?.tokenDecimals ?? selectedTokenData?.decimals ?? 18 - - const localPrice = - payload.currencyAmount && payload.currency - ? { amount: payload.currencyAmount, currency: payload.currency.code } - : { amount: payload.tokenAmount, currency: 'USD' } - const createChargeRequestPayload: CreateChargeRequest = { - pricing_type: 'fixed_price', - local_price: localPrice, - baseUrl: window.location.origin, - requestId: validRequestId, - requestProps: { - chainId: recipientChainId, - tokenAmount: payload.tokenAmount, - tokenAddress: recipientTokenAddress, - tokenType: isNativeCurrency(recipientTokenAddress) - ? peanutInterfaces.EPeanutLinkType.native - : peanutInterfaces.EPeanutLinkType.erc20, - tokenSymbol: recipientTokenSymbol, - tokenDecimals: Number(recipientTokenDecimals), - recipientAddress: payload.recipient?.resolvedAddress, - }, - transactionType: payload.transactionType, - } - - // add attachment if present - if (payload.attachmentOptions?.rawFile) { - createChargeRequestPayload.attachment = payload.attachmentOptions.rawFile - createChargeRequestPayload.filename = payload.attachmentOptions.rawFile.name - } - if (payload.attachmentOptions?.message) { - createChargeRequestPayload.reference = payload.attachmentOptions.message - } - - if (payload.attachmentOptions?.rawFile?.type) { - createChargeRequestPayload.mimeType = payload.attachmentOptions.rawFile.type - } - - console.log('Creating charge with payload:', createChargeRequestPayload) - const charge: TCharge = await chargesApi.create(createChargeRequestPayload) - console.log('Charge created response:', charge) - - if (!charge.data.id) { - console.error('CRITICAL: Charge created but UUID (ID) is missing!', charge.data) - throw new Error('Charge created successfully, but is missing a UUID.') - } - - // fetch the charge using the correct ID field from the response - chargeDetailsToUse = await chargesApi.get(charge.data.id) - console.log('Fetched charge details:', chargeDetailsToUse) - - dispatch(paymentActions.setChargeDetails(chargeDetailsToUse)) - setCreatedChargeDetails(chargeDetailsToUse) // keep track of the newly created charge - chargeCreated = true - - // update URL - const currentUrl = new URL(window.location.href) - if (currentUrl.searchParams.get('chargeId') !== chargeDetailsToUse.uuid) { - const newUrl = new URL(window.location.href) - if (payload.requestId) newUrl.searchParams.delete('id') - newUrl.searchParams.set('chargeId', chargeDetailsToUse.uuid) - // Use router.push (not window.history.replaceState) so that - // the components using the search params will be updated - router.push(newUrl.pathname + newUrl.search) - console.log('Updated URL with chargeId:', newUrl.href) - } - } - - // ensure we have charge details to proceed - if (!chargeDetailsToUse) { - console.error('Charge details are null after determination step.') - throw new Error('Failed to load or create charge details.') - } - - return { chargeDetails: chargeDetailsToUse, chargeCreated } - }, - [dispatch, requestDetails, activeChargeDetails, selectedTokenData, selectedChainID, selectedTokenAddress] - ) - - // helper function: Handle Peanut Wallet payment - const handlePeanutWalletPayment = useCallback( - async (chargeDetails: TRequestChargeResponse): Promise => { - setLoadingStep('Preparing Transaction') - - // validate required properties for preparing the transaction. - if ( - !chargeDetails.requestLink?.recipientAddress || - !chargeDetails.tokenAddress || - !chargeDetails.tokenAmount || - chargeDetails.tokenDecimals === undefined || - chargeDetails.tokenType === undefined - ) { - console.error('Charge data is missing required properties for transaction preparation:', chargeDetails) - throw new Error('Charge data is missing required properties for transaction preparation.') - } - - let receipt: TransactionReceipt | null - let userOpHash: Hash - const transactionsToSend = xChainUnsignedTxs ?? (unsignedTx ? [unsignedTx] : null) - if (transactionsToSend && transactionsToSend.length > 0) { - setLoadingStep('Sending Transaction') - const txResult = await sendTransactions(transactionsToSend, PEANUT_WALLET_CHAIN.id.toString()) - receipt = txResult.receipt - userOpHash = txResult.userOpHash - } else if ( - areEvmAddressesEqual(chargeDetails.tokenAddress, PEANUT_WALLET_TOKEN) && - chargeDetails.chainId === PEANUT_WALLET_CHAIN.id.toString() - ) { - const txResult = await sendMoney( - chargeDetails.requestLink.recipientAddress as `0x${string}`, - chargeDetails.tokenAmount - ) - receipt = txResult.receipt - userOpHash = txResult.userOpHash - } else { - console.error('No transaction prepared to send for peanut wallet.') - throw new Error('No transaction prepared to send.') - } - - // validation of the received receipt. - if (receipt !== null && isTxReverted(receipt)) { - console.error('Transaction reverted according to receipt:', receipt) - throw new Error(`Transaction failed (reverted). Hash: ${receipt.transactionHash}`) - } - - const txHash = receipt?.transactionHash ?? userOpHash - // update payment status in the backend api. - setLoadingStep('Updating Payment Status') - // peanut wallet flow: payer is the peanut wallet itself - const payment: PaymentCreationResponse = await chargesApi.createPayment({ - chargeId: chargeDetails.uuid, - chainId: PEANUT_WALLET_CHAIN.id.toString(), - hash: txHash, - tokenAddress: PEANUT_WALLET_TOKEN, - payerAddress: peanutWalletAddress ?? '', - }) - console.log('Backend payment creation response:', payment) - - setPaymentDetails(payment) - dispatch(paymentActions.setPaymentDetails(payment)) - dispatch(paymentActions.setTransactionHash(txHash)) - - setLoadingStep('Success') - console.log('Peanut Wallet payment successful.') - return { status: 'Success', charge: chargeDetails, payment, txHash: txHash, success: true } - }, - [sendTransactions, xChainUnsignedTxs, unsignedTx, peanutWalletAddress] - ) - - // helper function: Handle External Wallet payment - const handleExternalWalletPayment = useCallback( - async (chargeDetails: TRequestChargeResponse): Promise => { - const sourceChainId = Number(selectedChainID) - const connectedChainId = connectedWalletChain?.id - console.log(`Selected chain: ${sourceChainId}, Connected chain: ${connectedChainId}`) - - if (connectedChainId !== undefined && sourceChainId !== connectedChainId) { - console.log(`Switching network from ${connectedChainId} to ${sourceChainId}`) - setLoadingStep('Switching Network') - try { - await switchChainAsync({ chainId: sourceChainId }) - console.log(`Network switched successfully to ${sourceChainId}`) - } catch (switchError: any) { - console.error('Wallet network switch failed:', switchError) - const message = - switchError.shortMessage || - `Failed to switch network to chain ${sourceChainId}. Please switch manually in your wallet.` - throw new Error(message) // throw error, to be caught by main initiatePayment function - } - } - - const transactionsToSend = xChainUnsignedTxs ?? (unsignedTx ? [unsignedTx] : null) - if (!transactionsToSend || transactionsToSend.length === 0) { - console.error('No transaction prepared to send for external wallet.') - throw new Error('No transaction prepared to send.') - } - console.log('Transactions prepared for sending:', transactionsToSend) - - let receipt: TransactionReceipt - const receipts: TransactionReceipt[] = [] - let currentStep = 'Sending Transaction' - - try { - for (let i = 0; i < transactionsToSend.length; i++) { - const tx = transactionsToSend[i] - console.log(`Sending transaction ${i + 1}/${transactionsToSend.length}:`, tx) - setLoadingStep(`Sending Transaction`) - currentStep = 'Sending Transaction' - - const txGasOptions: any = {} - console.log('Using gas options:', txGasOptions) - - const hash = await sendTransactionAsync({ - to: (tx.to ? tx.to : undefined) as `0x${string}` | undefined, - value: tx.value ? BigInt(tx.value.toString()) : undefined, - data: tx.data ? (tx.data as `0x${string}`) : undefined, - ...txGasOptions, - chainId: sourceChainId, - }) - console.log(`Transaction ${i + 1} hash: ${hash}`) - - setLoadingStep(`Confirming Transaction`) - currentStep = 'Confirming Transaction' - - const txReceipt = await waitForTransactionReceipt(config, { - hash: hash, - chainId: sourceChainId, - confirmations: 1, - }) - console.log(`Transaction ${i + 1} receipt:`, txReceipt) - receipts.push(txReceipt) - - if (isTxReverted(txReceipt)) { - console.error(`Transaction ${i + 1} reverted:`, txReceipt) - throw new Error(`Transaction ${i + 1} failed (reverted).`) - } - } - // check if receipts were actually generated - if (receipts.length === 0 || !receipts[receipts.length - 1]) { - console.error('Transaction sequence completed, but failed to get final receipt.') - throw new Error('Transaction sent but failed to get receipt.') - } - receipt = receipts[receipts.length - 1] // use the last receipt - } catch (txError: any) { - // re-throw the error with the current step context - console.error(`Transaction failed during ${currentStep}:`, txError) - throw txError - } - - const txHash = receipt.transactionHash - setTransactionHash(txHash) - dispatch(paymentActions.setTransactionHash(txHash)) - console.log('External wallet final transaction hash:', txHash) - - setLoadingStep('Updating Payment Status') - console.log('Updating payment status in backend for external wallet. Hash:', txHash) - // external wallet / add-money flow: payer is the connected wallet address - const payment = await chargesApi.createPayment({ - chargeId: chargeDetails.uuid, - chainId: sourceChainId.toString(), - hash: txHash, - tokenAddress: selectedTokenData?.address || chargeDetails.tokenAddress, - payerAddress: wagmiAddress ?? '', - }) - console.log('Backend payment creation response:', payment) - - setPaymentDetails(payment) - dispatch(paymentActions.setPaymentDetails(payment)) - - setLoadingStep('Success') - console.log('External wallet payment successful.') - return { status: 'Success', charge: chargeDetails, payment, txHash, success: true } - }, - [ - dispatch, - selectedChainID, - connectedWalletChain, - switchChainAsync, - xChainUnsignedTxs, - unsignedTx, - sendTransactionAsync, - config, - selectedTokenData, - wagmiAddress, - ] - ) - - // @dev TODO: Refactor to TanStack Query mutation for architectural consistency - // Current: This async function works correctly (protected by isProcessing state) - // but is NOT tracked by usePendingTransactions mutation system. - // Future improvement: Wrap in useMutation for consistency with other balance-decreasing ops. - // mutationKey: [BALANCE_DECREASE, INITIATE_PAYMENT] - // Complexity: HIGH - complex state/Redux integration. Low priority. - // - // initiate and process payments - const initiatePayment = useCallback( - async (payload: InitiatePaymentPayload): Promise => { - setLoadingStep('Idle') - setError(null) - setTransactionHash(null) - setPaymentDetails(null) - - let determinedChargeDetails: TRequestChargeResponse | null = null - let chargeCreated = false - - try { - // 1. determine Charge Details - const { chargeDetails, chargeCreated: created } = await determineChargeDetails(payload) - determinedChargeDetails = chargeDetails - chargeCreated = created - console.log('Proceeding with charge details:', determinedChargeDetails.uuid) - - // 2. handle charge state - if ( - payload.returnAfterChargeCreation || // For request pot payment, return after charge creation - (chargeCreated && - (payload.isExternalWalletFlow || - !isPeanutWallet || - (isPeanutWallet && - (!areEvmAddressesEqual(determinedChargeDetails.tokenAddress, PEANUT_WALLET_TOKEN) || - determinedChargeDetails.chainId !== PEANUT_WALLET_CHAIN.id.toString())))) - ) { - console.log( - `Charge created. Transitioning to Confirm view for: ${ - payload.isExternalWalletFlow ? 'Add Money Flow' : 'External Wallet' - }.` - ) - setLoadingStep('Charge Created') - return { status: 'Charge Created', charge: determinedChargeDetails, success: false } - } - - // 3. if user is on the initial screen and chargeid is present, execute the handle charge state flow - if (payload.isExternalWalletFlow && currentView === 'INITIAL' && payload.chargeId) { - console.log('Executing add money flow: ChargeID already exists') - setLoadingStep('Charge Created') - - return { status: 'Charge Created', charge: determinedChargeDetails, success: false } - } - - // 4. execute payment based on wallet type - if (payload.isExternalWalletFlow) { - if (!wagmiAddress) { - console.error('Add Money flow requires an external wallet (WAGMI) to be connected.') - throw new Error('External wallet not connected for Add Money flow.') - } - console.log('Executing External Wallet transaction for Add Money flow.') - // Ensure charge details are passed, even if just created. - if (!determinedChargeDetails) - throw new Error('Charge details missing for Add Money external payment.') - return await handleExternalWalletPayment(determinedChargeDetails) - } else if (isPeanutWallet && peanutWalletAddress) { - console.log(`Executing Peanut Wallet transaction (chargeCreated: ${chargeCreated})`) - return await handlePeanutWalletPayment(determinedChargeDetails) - } else if (!isPeanutWallet) { - console.log('Handling payment for External Wallet (non-AddMoney, called from Confirm view).') - if (!determinedChargeDetails) throw new Error('Charge details missing for External Wallet payment.') - return await handleExternalWalletPayment(determinedChargeDetails) - } else { - console.error('Invalid payment state: Could not determine wallet type or required action.') - throw new Error('Invalid payment state: Could not determine wallet type or required action.') - } - } catch (err) { - // Ensure chargeId is removed from URL if error occurs after creation attempt - if (chargeCreated && determinedChargeDetails) { - console.log('Error occurred after charge creation, removing chargeId from URL.') - const currentUrl = new URL(window.location.href) - if (currentUrl.searchParams.get('chargeId') === determinedChargeDetails.uuid) { - const newUrl = new URL(window.location.href) - newUrl.searchParams.delete('chargeId') - // Use router.push (not window.history.replaceState) so that - // the components using the search params will be updated - router.push(newUrl.pathname + newUrl.search) - console.log('URL updated, chargeId removed.') - } - } - // handleError already logs the error and sets state - return handleError(err, loadingStep) - } - }, - [ - determineChargeDetails, - handlePeanutWalletPayment, - handleExternalWalletPayment, - isPeanutWallet, - peanutWalletAddress, - wagmiAddress, - handleError, - setLoadingStep, - setError, - router, - setTransactionHash, - setPaymentDetails, - loadingStep, - ] - ) - - const cancelOperation = useCallback(() => { - setError('Please confirm the request in your wallet.') - setLoadingStep('Error') - }, [setError, setLoadingStep]) - - const initiateDaimoPayment = useCallback( - async (payload: InitiatePaymentPayload) => { - try { - console.log('handleDaimoPayment', payload) - let determinedChargeDetails: TRequestChargeResponse | null = null - const { chargeDetails } = await determineChargeDetails(payload) - - determinedChargeDetails = chargeDetails - console.log('Proceeding with charge details:', determinedChargeDetails.uuid) - return { status: 'Charge Created', charge: determinedChargeDetails, success: false } - } catch (err) { - return handleError(err, loadingStep) - } - }, - [determineChargeDetails, setLoadingStep, setPaymentDetails] - ) - - const completeDaimoPayment = useCallback( - async ({ - chargeDetails, - destinationchainId, - txHash, - payerAddress, - sourceChainId, - sourceTokenAddress, - sourceTokenSymbol, - }: { - chargeDetails: TRequestChargeResponse - txHash: string - destinationchainId: number - payerAddress: string - sourceChainId: number - sourceTokenAddress: string - sourceTokenSymbol: string - }) => { - try { - setLoadingStep('Updating Payment Status') - const payment = await chargesApi.createPayment({ - chargeId: chargeDetails.uuid, - chainId: destinationchainId.toString(), - hash: txHash, - tokenAddress: chargeDetails.tokenAddress, - payerAddress, - sourceChainId: sourceChainId?.toString(), - sourceTokenAddress, - sourceTokenSymbol, - }) - - setPaymentDetails(payment) - dispatch(paymentActions.setPaymentDetails(payment)) - - setLoadingStep('Success') - console.log('Daimo payment successful.') - return { status: 'Success', charge: chargeDetails, payment, txHash, success: true } - } catch (err) { - return handleError(err, loadingStep) - } - }, - [determineChargeDetails, setLoadingStep, setPaymentDetails] - ) - - return { - initiatePayment, - prepareTransactionDetails, - isProcessing, - isPreparingTx, - loadingStep, - setLoadingStep, - error, - activeChargeDetails, - transactionHash, - paymentDetails, - slippagePercentage, - estimatedFromValue, - xChainUnsignedTxs, - estimatedGasCostUsd, - unsignedTx, - isCalculatingFees, - isFeeEstimationError, - isEstimatingGas, - isXChain, - diffTokens, - cancelOperation, - xChainRoute, - reset, - completeDaimoPayment, - initiateDaimoPayment, - } -} diff --git a/src/hooks/useRedirectQrStatus.ts b/src/hooks/useRedirectQrStatus.ts index ee299f592..732cca469 100644 --- a/src/hooks/useRedirectQrStatus.ts +++ b/src/hooks/useRedirectQrStatus.ts @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query' -import { PEANUT_API_URL } from '@/constants' +import { PEANUT_API_URL } from '@/constants/general.consts' interface RedirectQrStatusData { claimed: boolean diff --git a/src/hooks/useTokenPrice.ts b/src/hooks/useTokenPrice.ts index 8ad152162..da78ac6ab 100644 --- a/src/hooks/useTokenPrice.ts +++ b/src/hooks/useTokenPrice.ts @@ -1,18 +1,17 @@ import { useQuery } from '@tanstack/react-query' import { fetchTokenPrice } from '@/app/actions/tokens' import { + PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN, PEANUT_WALLET_TOKEN_DECIMALS, PEANUT_WALLET_TOKEN_SYMBOL, PEANUT_WALLET_TOKEN_NAME, - PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN_IMG_URL, - STABLE_COINS, - supportedMobulaChains, -} from '@/constants' +} from '@/constants/zerodev.consts' import { type ITokenPriceData } from '@/interfaces' import * as Sentry from '@sentry/nextjs' import { interfaces } from '@squirrel-labs/peanut-sdk' +import { STABLE_COINS, supportedMobulaChains } from '@/constants/general.consts' interface UseTokenPriceParams { tokenAddress: string | undefined diff --git a/src/hooks/useTransactionHistory.ts b/src/hooks/useTransactionHistory.ts index 87f078dbf..dbf841f7b 100644 --- a/src/hooks/useTransactionHistory.ts +++ b/src/hooks/useTransactionHistory.ts @@ -1,11 +1,11 @@ -import { PEANUT_API_URL } from '@/constants' import { TRANSACTIONS } from '@/constants/query.consts' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import type { InfiniteData, InfiniteQueryObserverResult, QueryObserverResult } from '@tanstack/react-query' import { useInfiniteQuery, useQuery } from '@tanstack/react-query' import Cookies from 'js-cookie' import { completeHistoryEntry } from '@/utils/history.utils' import type { HistoryEntry } from '@/utils/history.utils' +import { PEANUT_API_URL } from '@/constants/general.consts' //TODO: remove and import all from utils everywhere export { EHistoryEntryType, EHistoryUserRole } from '@/utils/history.utils' diff --git a/src/hooks/useUserByUsername.ts b/src/hooks/useUserByUsername.ts index 2661a4fc7..4d1ebe77a 100644 --- a/src/hooks/useUserByUsername.ts +++ b/src/hooks/useUserByUsername.ts @@ -8,7 +8,7 @@ import { type ApiUser, usersApi } from '@/services/users' */ export const useUserByUsername = (username: string | null | undefined) => { const [user, setUser] = useState(null) - const [isLoading, setIsLoading] = useState(false) + const [isLoading, setIsLoading] = useState(!!username) const [error, setError] = useState(null) useEffect(() => { diff --git a/src/hooks/useZeroDev.ts b/src/hooks/useZeroDev.ts index c7cb01450..c8518ecd6 100644 --- a/src/hooks/useZeroDev.ts +++ b/src/hooks/useZeroDev.ts @@ -6,7 +6,8 @@ import { useAuth } from '@/context/authContext' import { useKernelClient } from '@/context/kernelClient.context' import { useAppDispatch, useSetupStore, useZerodevStore } from '@/redux/hooks' import { zerodevActions } from '@/redux/slices/zerodev-slice' -import { getFromCookie, removeFromCookie, saveToCookie, clearAuthState } from '@/utils' +import { getFromCookie, removeFromCookie, saveToCookie } from '@/utils/general.utils' +import { clearAuthState } from '@/utils/auth.utils' import { toWebAuthnKey, WebAuthnMode } from '@zerodev/passkey-validator' import { useCallback, useContext } from 'react' import type { TransactionReceipt, Hex, Hash } from 'viem' diff --git a/src/hooks/wallet/__tests__/useSendMoney.test.tsx b/src/hooks/wallet/__tests__/useSendMoney.test.tsx index 89c32dc9a..d8fe210f3 100644 --- a/src/hooks/wallet/__tests__/useSendMoney.test.tsx +++ b/src/hooks/wallet/__tests__/useSendMoney.test.tsx @@ -8,16 +8,16 @@ * 4. Balance invalidation on success */ -import { renderHook, waitFor } from '@testing-library/react' +import { renderHook, waitFor, act } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ToastProvider } from '@/components/0_Bruddle/Toast' import { useSendMoney } from '../useSendMoney' import { parseUnits } from 'viem' -import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants' +import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' import type { ReactNode } from 'react' // Mock dependencies -jest.mock('@/constants', () => ({ +jest.mock('@/constants/zerodev.consts', () => ({ PEANUT_WALLET_TOKEN: '0x1234567890123456789012345678901234567890', PEANUT_WALLET_TOKEN_DECIMALS: 6, PEANUT_WALLET_CHAIN: { id: 137 }, @@ -72,20 +72,18 @@ describe('useSendMoney', () => { { wrapper } ) - // Trigger mutation - const promise = result.current.mutateAsync({ - toAddress: '0x9999999999999999999999999999999999999999' as `0x${string}`, - amountInUsd: amountToSend, - }) - - // Check optimistic update happened immediately - await waitFor(() => { - const currentBalance = queryClient.getQueryData(['balance', mockAddress]) - const expectedBalance = initialBalance - parseUnits(amountToSend, PEANUT_WALLET_TOKEN_DECIMALS) - expect(currentBalance).toEqual(expectedBalance) + // Trigger mutation and wait for completion + await act(async () => { + await result.current.mutateAsync({ + toAddress: '0x9999999999999999999999999999999999999999' as `0x${string}`, + amountInUsd: amountToSend, + }) }) - await promise + // After successful mutation, balance should reflect the deduction + const currentBalance = queryClient.getQueryData(['balance', mockAddress]) + const expectedBalance = initialBalance - parseUnits(amountToSend, PEANUT_WALLET_TOKEN_DECIMALS) + expect(currentBalance).toEqual(expectedBalance) }) it('should NOT optimistically update balance when insufficient balance (prevents underflow)', async () => { diff --git a/src/hooks/wallet/useBalance.ts b/src/hooks/wallet/useBalance.ts index ef6a89fa4..2d5611a28 100644 --- a/src/hooks/wallet/useBalance.ts +++ b/src/hooks/wallet/useBalance.ts @@ -1,7 +1,8 @@ +import { PEANUT_WALLET_TOKEN } from '@/constants/zerodev.consts' +import { peanutPublicClient } from '@/app/actions/clients' import { useQuery } from '@tanstack/react-query' import { erc20Abi } from 'viem' import type { Address } from 'viem' -import { PEANUT_WALLET_TOKEN, peanutPublicClient } from '@/constants' /** * Hook to fetch and auto-refresh wallet balance using TanStack Query diff --git a/src/hooks/wallet/useSendMoney.ts b/src/hooks/wallet/useSendMoney.ts index 2e4d5e9a0..d64ce3a07 100644 --- a/src/hooks/wallet/useSendMoney.ts +++ b/src/hooks/wallet/useSendMoney.ts @@ -1,7 +1,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { parseUnits, encodeFunctionData, erc20Abi } from 'viem' import type { Address, Hash, Hex, TransactionReceipt } from 'viem' -import { PEANUT_WALLET_TOKEN, PEANUT_WALLET_TOKEN_DECIMALS, PEANUT_WALLET_CHAIN } from '@/constants' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN, PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' import { TRANSACTIONS, BALANCE_DECREASE, SEND_MONEY } from '@/constants/query.consts' import { useToast } from '@/components/0_Bruddle/Toast' diff --git a/src/hooks/wallet/useWallet.ts b/src/hooks/wallet/useWallet.ts index 361c23b38..64c796e38 100644 --- a/src/hooks/wallet/useWallet.ts +++ b/src/hooks/wallet/useWallet.ts @@ -1,16 +1,17 @@ 'use client' -import { PEANUT_WALLET_CHAIN } from '@/constants' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' import { useAppDispatch, useWalletStore } from '@/redux/hooks' import { walletActions } from '@/redux/slices/wallet-slice' import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' -import { useCallback, useEffect } from 'react' -import type { Hex, Address } from 'viem' +import { useCallback, useEffect, useMemo } from 'react' +import { formatUnits, type Hex, type Address } from 'viem' import { useZeroDev } from '../useZeroDev' import { useAuth } from '@/context/authContext' import { AccountType } from '@/interfaces' import { useBalance } from './useBalance' import { useSendMoney as useSendMoneyMutation } from './useSendMoney' +import { formatCurrency } from '@/utils/general.utils' export const useWallet = () => { const dispatch = useAppDispatch() @@ -89,9 +90,29 @@ export const useWallet = () => { // consider balance as fetching until: address is validated and query has resolved const isBalanceLoading = !isAddressReady || isFetchingBalance + // formatted balance for display (e.g. "1,234.56") + const formattedBalance = useMemo(() => { + if (balance === undefined) return '0.00' + return formatCurrency(formatUnits(balance, PEANUT_WALLET_TOKEN_DECIMALS)) + }, [balance]) + + // check if wallet has sufficient balance for a given usd amount + const hasSufficientBalance = useCallback( + (amountUsd: string | number): boolean => { + if (balance === undefined) return false + const amount = typeof amountUsd === 'string' ? parseFloat(amountUsd) : amountUsd + if (isNaN(amount) || amount < 0) return false + const amountInWei = BigInt(Math.floor(amount * 10 ** PEANUT_WALLET_TOKEN_DECIMALS)) + return balance >= amountInWei + }, + [balance] + ) + return { address: isAddressReady ? address : undefined, // populate address only if it is validated and matches the user's wallet address balance, + formattedBalance, + hasSufficientBalance, isConnected: isKernelClientReady, sendTransactions, sendMoney, diff --git a/src/interfaces/attachment.ts b/src/interfaces/attachment.ts new file mode 100644 index 000000000..9b6e0864f --- /dev/null +++ b/src/interfaces/attachment.ts @@ -0,0 +1,11 @@ +/** + * shared attachment options type used across payment flows + * + * allows users to attach a message and/or file to payments. + * fileUrl is the uploaded url, rawFile is the original file object. + */ +export interface IAttachmentOptions { + fileUrl: string | undefined + message: string | undefined + rawFile: File | undefined +} diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 27b19a490..6d8df29c1 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -1,8 +1,10 @@ -import { type BridgeKycStatus } from '@/utils' import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' export type RecipientType = 'address' | 'ens' | 'iban' | 'us' | 'username' +// Moved here from bridge-accounts.utils.ts to avoid circular dependency +export type BridgeKycStatus = 'not_started' | 'under_review' | 'approved' | 'rejected' | 'incomplete' + export interface IResponse { success: boolean data?: any diff --git a/src/interfaces/wallet.interfaces.ts b/src/interfaces/wallet.interfaces.ts index 505a8aed6..dcdbd4479 100644 --- a/src/interfaces/wallet.interfaces.ts +++ b/src/interfaces/wallet.interfaces.ts @@ -1,10 +1,11 @@ -import * as interfaces from '@/interfaces' - // based on API AccountType + +import { AccountType, type IUserBalance } from './interfaces' + // https://github.com/peanutprotocol/peanut-api-ts/blob/b32570b7bd366efed7879f607040c511fa036a57/src/db/interfaces/account.ts export enum WalletProviderType { - PEANUT = interfaces.AccountType.PEANUT_WALLET, - BYOW = interfaces.AccountType.EVM_ADDRESS, + PEANUT = AccountType.PEANUT_WALLET, + BYOW = AccountType.EVM_ADDRESS, REWARDS = 'rewards', } @@ -34,7 +35,7 @@ export interface IWallet extends IDBWallet { id: string connected: boolean balance: bigint - balances?: interfaces.IUserBalance[] + balances?: IUserBalance[] } export enum WalletErrorType { diff --git a/src/lib/url-parser/parser.ts b/src/lib/url-parser/parser.ts index 0cd0c247b..f04feeb0b 100644 --- a/src/lib/url-parser/parser.ts +++ b/src/lib/url-parser/parser.ts @@ -1,5 +1,5 @@ import { getSquidChainsAndTokens } from '@/app/actions/squid' -import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants/zerodev.consts' import { interfaces } from '@squirrel-labs/peanut-sdk' import { validateAmount } from '../validation/amount' import { validateAndResolveRecipient } from '../validation/recipient' @@ -159,23 +159,13 @@ export async function parsePaymentURL( tokenDetails = chainDetails.tokens.find((t) => t.symbol.toLowerCase() === 'USDC'.toLowerCase()) } - // 6. Determine if this is a DevConnect flow - // @dev: note, this needs to be deleted post devconnect - // devconnect flow: external address + base chain specified in URL - const isDevConnectFlow = - recipientDetails.recipientType === 'ADDRESS' && - chainId !== undefined && - chainId.toLowerCase() === 'base' && - chainDetails !== undefined - - // 7. Construct and return the final result + // 6. Construct and return the final result return { parsedUrl: { recipient: recipientDetails, amount: parsedAmount?.amount, token: tokenDetails, chain: chainDetails, - isDevConnectFlow, }, error: null, } diff --git a/src/lib/url-parser/types/payment.ts b/src/lib/url-parser/types/payment.ts index 37f7d46ce..1c7a9b31e 100644 --- a/src/lib/url-parser/types/payment.ts +++ b/src/lib/url-parser/types/payment.ts @@ -13,6 +13,4 @@ export interface ParsedURL { amount?: string token?: interfaces.ISquidToken chain?: interfaces.ISquidChain & { tokens: interfaces.ISquidToken[] } - /** @dev: flag indicating if this is a devconnect flow (external address + base chain), to be deleted post devconnect */ - isDevConnectFlow?: boolean } diff --git a/src/lib/validation/recipient.test.ts b/src/lib/validation/recipient.test.ts index 41788ee76..ceae06feb 100644 --- a/src/lib/validation/recipient.test.ts +++ b/src/lib/validation/recipient.test.ts @@ -13,11 +13,11 @@ jest.mock('@/app/actions/ens', () => ({ }, })) -jest.mock('@/utils', () => ({ +jest.mock('@/utils/sentry.utils', () => ({ fetchWithSentry: jest.fn(), })) -jest.mock('@/constants', () => ({ +jest.mock('@/constants/general.consts', () => ({ JUSTANAME_ENS: 'testvc.eth', PEANUT_API_URL: process.env.NEXT_PUBLIC_PEANUT_API_URL, })) @@ -78,7 +78,7 @@ describe('Recipient Validation', () => { it('should throw for invalid Peanut usernames', async () => { // Mock failed API response - const fetchWithSentry = require('@/utils').fetchWithSentry + const { fetchWithSentry } = require('@/utils/sentry.utils') fetchWithSentry.mockResolvedValueOnce({ status: 404 }) await expect(validateAndResolveRecipient('lmaoo')).rejects.toThrow('Invalid Peanut username') @@ -92,7 +92,7 @@ describe('Recipient Validation', () => { describe('verifyPeanutUsername', () => { it('should return true for valid usernames', async () => { - const fetchWithSentry = require('@/utils').fetchWithSentry + const { fetchWithSentry } = require('@/utils/sentry.utils') fetchWithSentry.mockResolvedValueOnce({ status: 200 }) const result = await verifyPeanutUsername('kusharc') @@ -100,7 +100,7 @@ describe('Recipient Validation', () => { }) it('should return false for invalid usernames', async () => { - const fetchWithSentry = require('@/utils').fetchWithSentry + const { fetchWithSentry } = require('@/utils/sentry.utils') fetchWithSentry.mockResolvedValueOnce({ status: 404 }) const result = await verifyPeanutUsername('invaliduser') @@ -108,7 +108,7 @@ describe('Recipient Validation', () => { }) it('should handle API errors gracefully', async () => { - const fetchWithSentry = require('@/utils').fetchWithSentry + const { fetchWithSentry } = require('@/utils/sentry.utils') fetchWithSentry.mockRejectedValueOnce(new Error('API Error')) const result = await verifyPeanutUsername('someuser') diff --git a/src/lib/validation/recipient.ts b/src/lib/validation/recipient.ts index e7427c33a..429f3dcab 100644 --- a/src/lib/validation/recipient.ts +++ b/src/lib/validation/recipient.ts @@ -1,13 +1,14 @@ import { isAddress } from 'viem' import { resolveEns } from '@/app/actions/ens' -import { PEANUT_API_URL } from '@/constants' + import { AccountType } from '@/interfaces' import { usersApi } from '@/services/users' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import * as Sentry from '@sentry/nextjs' import { RecipientValidationError } from '../url-parser/errors' import { type RecipientType } from '../url-parser/types/payment' +import { PEANUT_API_URL } from '@/constants/general.consts' export async function validateAndResolveRecipient( recipient: string, diff --git a/src/lib/validation/token.test.ts b/src/lib/validation/token.test.ts index 02db056da..40064dc97 100644 --- a/src/lib/validation/token.test.ts +++ b/src/lib/validation/token.test.ts @@ -95,7 +95,7 @@ const mockSquidChains: Record< }, } -jest.mock('@/constants', () => ({ +jest.mock('@/constants/zerodev.consts', () => ({ PEANUT_WALLET_CHAIN: { id: '1', name: 'Ethereum', diff --git a/src/lib/validation/token.ts b/src/lib/validation/token.ts index 33d43ebf0..a0500cd94 100644 --- a/src/lib/validation/token.ts +++ b/src/lib/validation/token.ts @@ -1,5 +1,5 @@ import { getSquidChainsAndTokens } from '@/app/actions/squid' -import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants/zerodev.consts' import { interfaces } from '@squirrel-labs/peanut-sdk' import { ChainValidationError } from '../url-parser/errors' import { POPULAR_CHAIN_NAME_VARIANTS } from '../url-parser/parser.consts' diff --git a/src/redux/constants/index.ts b/src/redux/constants/index.ts index 6a9b137fb..1ee1e58ed 100644 --- a/src/redux/constants/index.ts +++ b/src/redux/constants/index.ts @@ -1,7 +1,5 @@ export const SETUP = 'setup' export const WALLET_SLICE = 'wallet_slice' export const ZERODEV_SLICE = 'zerodev_slice' -export const PAYMENT_SLICE = 'payment_slice' export const AUTH_SLICE = 'auth_slice' -export const SEND_FLOW_SLICE = 'send_flow_slice' export const BANK_FORM_SLICE = 'bank_form_slice' diff --git a/src/redux/hooks.ts b/src/redux/hooks.ts index e551d90af..d8d7ef1f3 100644 --- a/src/redux/hooks.ts +++ b/src/redux/hooks.ts @@ -5,10 +5,8 @@ import { type AppDispatch, type RootState } from './types' export const useAppDispatch = () => useDispatch() export const useAppSelector: TypedUseSelectorHook = useSelector -// Selector hooks for utilization +// selector hooks for utilization export const useSetupStore = () => useAppSelector((state) => state.setup) export const useWalletStore = () => useAppSelector((state) => state.wallet) export const useZerodevStore = () => useAppSelector((state) => state.zeroDev) -export const usePaymentStore = () => useAppSelector((state) => state.payment) export const useUserStore = () => useAppSelector((state) => state.user) -export const useSendFlowStore = () => useAppSelector((state) => state.sendFlow) diff --git a/src/redux/slices/payment-slice.ts b/src/redux/slices/payment-slice.ts deleted file mode 100644 index e736048d6..000000000 --- a/src/redux/slices/payment-slice.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { type ParsedURL } from '@/lib/url-parser/types/payment' -import { - type PaymentCreationResponse, - type TCharge, - type TRequestChargeResponse, - type TRequestResponse, -} from '@/services/services.types' -import { createSlice, type PayloadAction } from '@reduxjs/toolkit' -import { PAYMENT_SLICE } from '../constants' -import { type IPaymentState, type TPaymentView } from '../types/payment.types' -import { type IAttachmentOptions } from '../types/send-flow.types' - -const initialState: IPaymentState = { - currentView: 'INITIAL', - attachmentOptions: { - fileUrl: '', - message: '', - rawFile: undefined, - }, - parsedPaymentData: null, - requestDetails: null, - chargeDetails: null, - createdChargeDetails: null, - transactionHash: null, - paymentDetails: null, - resolvedAddress: null, - error: null, - usdAmount: null, - daimoError: null, - isDaimoPaymentProcessing: false, -} - -const paymentSlice = createSlice({ - name: PAYMENT_SLICE, - initialState, - reducers: { - setView: (state, action: PayloadAction) => { - state.currentView = action.payload - }, - setAttachmentOptions: (state, action: PayloadAction) => { - state.attachmentOptions = action.payload - }, - setParsedPaymentData: (state, action: PayloadAction) => { - state.parsedPaymentData = action.payload - }, - setRequestDetails: (state, action: PayloadAction) => { - state.requestDetails = action.payload - }, - setChargeDetails: (state, action: PayloadAction) => { - state.chargeDetails = action.payload - }, - setCreatedChargeDetails: (state, action: PayloadAction) => { - state.createdChargeDetails = action.payload - }, - setTransactionHash: (state, action: PayloadAction) => { - state.transactionHash = action.payload - }, - setPaymentDetails: (state, action: PayloadAction) => { - state.paymentDetails = action.payload - }, - setResolvedAddress: (state, action: PayloadAction) => { - state.resolvedAddress = action.payload - }, - setError: (state, action: PayloadAction) => { - state.error = action.payload - }, - setUsdAmount: (state, action: PayloadAction) => { - state.usdAmount = action.payload - }, - setDaimoError: (state, action: PayloadAction) => { - state.daimoError = action.payload - }, - setIsDaimoPaymentProcessing: (state, action: PayloadAction) => { - state.isDaimoPaymentProcessing = action.payload - }, - resetPaymentState: (state) => { - return initialState - }, - }, -}) - -export const paymentActions = paymentSlice.actions -export default paymentSlice.reducer diff --git a/src/redux/slices/send-flow-slice.ts b/src/redux/slices/send-flow-slice.ts deleted file mode 100644 index 1b9df4a11..000000000 --- a/src/redux/slices/send-flow-slice.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit' -import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' -import { SEND_FLOW_SLICE } from '../constants' -import { - type ErrorState, - type IAttachmentOptions, - type ISendFlowState, - type Recipient, - type SendFlowTxnType, - type SendFlowView, -} from '../types/send-flow.types' - -const initialState: ISendFlowState = { - view: 'INITIAL', - tokenValue: undefined, - recipient: { - address: undefined, - name: undefined, - }, - usdValue: undefined, - linkDetails: undefined, - password: undefined, - transactionType: 'not-gasless', - estimatedPoints: undefined, - gaslessPayload: undefined, - gaslessPayloadMessage: undefined, - preparedDepositTxs: undefined, - txHash: undefined, - link: undefined, - transactionCostUSD: undefined, - attachmentOptions: { - fileUrl: undefined, - message: undefined, - rawFile: undefined, - }, - errorState: undefined, - crossChainDetails: undefined, -} - -const sendFlowSlice = createSlice({ - name: SEND_FLOW_SLICE, - initialState, - reducers: { - setView(state, action: { payload: SendFlowView }) { - state.view = action.payload - }, - setTokenValue(state, action: { payload: string | undefined }) { - state.tokenValue = action.payload - }, - setRecipient(state, action: { payload: Recipient }) { - state.recipient = action.payload - }, - setUsdValue(state, action: { payload: string | undefined }) { - state.usdValue = action.payload - }, - setLinkDetails(state, action: { payload: peanutInterfaces.IPeanutLinkDetails | undefined }) { - state.linkDetails = action.payload - }, - setPassword(state, action: { payload: string | undefined }) { - state.password = action.payload - }, - setTransactionType(state, action: { payload: SendFlowTxnType }) { - state.transactionType = action.payload - }, - setEstimatedPoints(state, action: { payload: number | undefined }) { - state.estimatedPoints = action.payload - }, - setGaslessPayload(state, action: { payload: peanutInterfaces.IGaslessDepositPayload | undefined }) { - state.gaslessPayload = action.payload - }, - setGaslessPayloadMessage(state, action: { payload: peanutInterfaces.IPreparedEIP712Message | undefined }) { - state.gaslessPayloadMessage = action.payload - }, - setPreparedDepositTxs(state, action: { payload: peanutInterfaces.IPrepareDepositTxsResponse | undefined }) { - state.preparedDepositTxs = action.payload - }, - setTxHash(state, action: { payload: string | undefined }) { - state.txHash = action.payload - }, - setLink(state, action: { payload: string | undefined }) { - state.link = action.payload - }, - setTransactionCostUSD(state, action: { payload: number | undefined }) { - state.transactionCostUSD = action.payload - }, - setAttachmentOptions(state, action: { payload: IAttachmentOptions }) { - state.attachmentOptions = action.payload - }, - setErrorState(state, action: { payload: ErrorState | undefined }) { - state.errorState = action.payload - }, - setCrossChainDetails(state, action: { payload: [] | undefined }) { - state.crossChainDetails = action.payload - }, - resetSendFlow: (state) => { - return initialState - }, - }, -}) - -export const sendFlowActions = sendFlowSlice.actions -export default sendFlowSlice.reducer diff --git a/src/redux/slices/wallet-slice.ts b/src/redux/slices/wallet-slice.ts index 787ebdbbe..4bc39443f 100644 --- a/src/redux/slices/wallet-slice.ts +++ b/src/redux/slices/wallet-slice.ts @@ -4,7 +4,6 @@ import { type WalletUIState } from '../types/wallet.types' import { type PayloadAction } from '@reduxjs/toolkit' const initialState: WalletUIState = { - signInModalVisible: false, balance: undefined, } @@ -12,9 +11,6 @@ const walletSlice = createSlice({ name: WALLET_SLICE, initialState, reducers: { - setSignInModalVisible: (state, action) => { - state.signInModalVisible = action.payload - }, setBalance: (state, action: PayloadAction) => { state.balance = action.payload.toString() }, diff --git a/src/redux/store.ts b/src/redux/store.ts index a3672945b..ea4bd3db0 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -1,6 +1,4 @@ import { configureStore } from '@reduxjs/toolkit' -import paymentReducer from './slices/payment-slice' -import sendFlowReducer from './slices/send-flow-slice' import setupReducer from './slices/setup-slice' import userReducer from './slices/user-slice' import walletReducer from './slices/wallet-slice' @@ -12,9 +10,7 @@ const store = configureStore({ setup: setupReducer, wallet: walletReducer, zeroDev: zeroDevReducer, - payment: paymentReducer, user: userReducer, - sendFlow: sendFlowReducer, bankForm: bankFormReducer, }, // disable redux serialization checks diff --git a/src/redux/types/payment.types.ts b/src/redux/types/payment.types.ts index a837cbf4c..f413c0c7f 100644 --- a/src/redux/types/payment.types.ts +++ b/src/redux/types/payment.types.ts @@ -5,7 +5,7 @@ import { type TRequestChargeResponse, type TRequestResponse, } from '@/services/services.types' -import { type IAttachmentOptions } from './send-flow.types' +import { type IAttachmentOptions } from '@/interfaces/attachment' export type TPaymentView = 'INITIAL' | 'CONFIRM' | 'STATUS' | 'PUBLIC_PROFILE' diff --git a/src/redux/types/send-flow.types.ts b/src/redux/types/send-flow.types.ts deleted file mode 100644 index f9445dd30..000000000 --- a/src/redux/types/send-flow.types.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' - -export type SendFlowView = 'INITIAL' | 'CONFIRM' | 'SUCCESS' | 'ERROR' - -export type SendFlowTxnType = 'not-gasless' | 'gasless' - -export type Recipient = { address: string | undefined; name: string | undefined } - -export type IAttachmentOptions = { - fileUrl: string | undefined - message: string | undefined - rawFile: File | undefined -} - -export type ErrorState = { - showError: boolean - errorMessage: string -} -export interface ISendFlowState { - view: SendFlowView - tokenValue: string | undefined - recipient: Recipient - usdValue: string | undefined - linkDetails: peanutInterfaces.IPeanutLinkDetails | undefined - password: string | undefined - transactionType: SendFlowTxnType - estimatedPoints: number | undefined - gaslessPayload: peanutInterfaces.IGaslessDepositPayload | undefined - gaslessPayloadMessage: peanutInterfaces.IPreparedEIP712Message | undefined - preparedDepositTxs: peanutInterfaces.IPrepareDepositTxsResponse | undefined - txHash: string | undefined - link: string | undefined - transactionCostUSD: number | undefined - attachmentOptions: IAttachmentOptions - errorState: ErrorState | undefined - crossChainDetails: [] | undefined -} diff --git a/src/redux/types/wallet.types.ts b/src/redux/types/wallet.types.ts index fc1947fc6..63e00b962 100644 --- a/src/redux/types/wallet.types.ts +++ b/src/redux/types/wallet.types.ts @@ -1,4 +1,3 @@ export interface WalletUIState { - signInModalVisible: boolean balance: string | undefined } diff --git a/src/services/charges.ts b/src/services/charges.ts index ec4397997..409a6f332 100644 --- a/src/services/charges.ts +++ b/src/services/charges.ts @@ -1,12 +1,13 @@ -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry, jsonParse } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { jsonParse } from '@/utils/general.utils' import Cookies from 'js-cookie' import { - type CreateChargeRequest, + type TRequestChargeResponse, type PaymentCreationResponse, type TCharge, - type TRequestChargeResponse, + type CreateChargeRequest, } from './services.types' +import { PEANUT_API_URL } from '@/constants/general.consts' export const chargesApi = { create: async (data: CreateChargeRequest): Promise => { diff --git a/src/services/invites.ts b/src/services/invites.ts index 624d3a7a3..6f069db51 100644 --- a/src/services/invites.ts +++ b/src/services/invites.ts @@ -1,8 +1,8 @@ import { validateInviteCode } from '@/app/actions/invites' -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import Cookies from 'js-cookie' import { EInviteType, type PointsInvitesResponse } from './services.types' +import { PEANUT_API_URL } from '@/constants/general.consts' export const invitesApi = { acceptInvite: async ( diff --git a/src/services/manteca.ts b/src/services/manteca.ts index 282d205e4..6f7f43941 100644 --- a/src/services/manteca.ts +++ b/src/services/manteca.ts @@ -1,11 +1,12 @@ -import { PEANUT_API_URL, PEANUT_API_KEY } from '@/constants' +import { PEANUT_API_URL, PEANUT_API_KEY } from '@/constants/general.consts' import { type MantecaDepositResponseData, type MantecaWithdrawData, type MantecaWithdrawResponse, type CreateMantecaOnrampParams, } from '@/types/manteca.types' -import { fetchWithSentry, jsonStringify } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { jsonStringify } from '@/utils/general.utils' import Cookies from 'js-cookie' import type { Address } from 'viem' import type { SignUserOperationReturnType } from '@zerodev/sdk/actions' @@ -253,7 +254,8 @@ export const mantecaApi = { Authorization: `Bearer ${Cookies.get('jwt-token')}`, }, body: jsonStringify({ - usdAmount: params.usdAmount, + amount: params.amount, + isUsdDenominated: params.isUsdDenominated, currency: params.currency, chargeId: params.chargeId, }), diff --git a/src/services/notifications.ts b/src/services/notifications.ts index 87532cef6..604d28b12 100644 --- a/src/services/notifications.ts +++ b/src/services/notifications.ts @@ -1,5 +1,5 @@ -import { PEANUT_API_URL } from '@/constants' import Cookies from 'js-cookie' +import { PEANUT_API_URL } from '@/constants/general.consts' export type InAppItem = { id: string diff --git a/src/services/points.ts b/src/services/points.ts index 7de61b0f2..562a21880 100644 --- a/src/services/points.ts +++ b/src/services/points.ts @@ -1,7 +1,7 @@ import Cookies from 'js-cookie' import { type CalculatePointsRequest, PointsAction, type TierInfo } from './services.types' -import { fetchWithSentry } from '@/utils' -import { PEANUT_API_URL } from '@/constants' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { PEANUT_API_URL } from '@/constants/general.consts' type InvitesGraphResponse = { success: boolean diff --git a/src/services/quests.ts b/src/services/quests.ts index d9066869d..431d77137 100644 --- a/src/services/quests.ts +++ b/src/services/quests.ts @@ -4,9 +4,9 @@ */ import Cookies from 'js-cookie' -import { fetchWithSentry } from '@/utils' -import { PEANUT_API_URL } from '@/constants' +import { fetchWithSentry } from '@/utils/sentry.utils' import type { QuestLeaderboardData, AllQuestsLeaderboardData } from '@/app/quests/types' +import { PEANUT_API_URL } from '@/constants/general.consts' export const questsApi = { /** diff --git a/src/services/requests.ts b/src/services/requests.ts index f7932af47..75d908180 100644 --- a/src/services/requests.ts +++ b/src/services/requests.ts @@ -1,7 +1,8 @@ -import { PEANUT_API_URL } from '@/constants' import { type CreateRequestRequest, type TRequestResponse } from './services.types' -import { fetchWithSentry, jsonStringify } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { jsonStringify } from '@/utils/general.utils' import Cookies from 'js-cookie' +import { PEANUT_API_URL } from '@/constants/general.consts' export const requestsApi = { create: async (data: CreateRequestRequest): Promise => { diff --git a/src/services/rewards.ts b/src/services/rewards.ts index 3b29313d6..8c1868c09 100644 --- a/src/services/rewards.ts +++ b/src/services/rewards.ts @@ -1,7 +1,7 @@ -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { type RewardLink } from './services.types' import Cookies from 'js-cookie' +import { PEANUT_API_URL } from '@/constants/general.consts' export const rewardsApi = { getByUser: async (userId: string): Promise => { diff --git a/src/services/rhino.ts b/src/services/rhino.ts new file mode 100644 index 000000000..05df247d5 --- /dev/null +++ b/src/services/rhino.ts @@ -0,0 +1,89 @@ +import type { CreateDepositAddressResponse, RhinoChainType } from './services.types' +import { PEANUT_API_URL } from '@/constants/general.consts' +import Cookies from 'js-cookie' + +export const rhinoApi = { + createDepositAddress: async ( + destinationAddress: string, + chainType: RhinoChainType, + identifier: string + ): Promise => { + const token = Cookies.get('jwt-token') + if (!token) { + throw new Error('Authentication required') + } + + const response = await fetch(`${PEANUT_API_URL}/rhino/deposit`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ destinationAddress, type: chainType, addressNote: identifier }), + }) + if (!response.ok) { + throw new Error(`Failed to fetch deposit address: ${response.statusText}`) + } + const data = await response.json() + return data as CreateDepositAddressResponse + }, + + getDepositAddressStatus: async (depositAddress: string): Promise<{ status: string; amount?: number }> => { + const response = await fetch(`${PEANUT_API_URL}/rhino/status/${depositAddress}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + throw new Error(`Failed to fetch deposit address status: ${response.statusText}`) + } + + const data = await response.json() + return data + }, + + resetDepositAddressStatus: async (depositAddress: string): Promise => { + const token = Cookies.get('jwt-token') + if (!token) { + throw new Error('Authentication required') + } + const response = await fetch(`${PEANUT_API_URL}/rhino/reset-status/${depositAddress}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }) + + if (!response.ok) { + throw new Error(`Failed to update deposit address status: ${response.statusText}`) + } + + return true + }, + + createRequestFulfilmentAddress: async ( + chainType: RhinoChainType, + chargeId: string, + peanutWalletAddress?: string + ): Promise => { + const response = await fetch(`${PEANUT_API_URL}/rhino/request-fulfilment`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + type: chainType, + chargeId, + senderPeanutWalletAddress: peanutWalletAddress, + }), + }) + if (!response.ok) { + throw new Error(`Failed to fetch Request Fulfilment Address: ${response.statusText}`) + } + const data = await response.json() + return data as CreateDepositAddressResponse + }, +} diff --git a/src/services/sendLinks.ts b/src/services/sendLinks.ts index b8ba670e2..38150e112 100644 --- a/src/services/sendLinks.ts +++ b/src/services/sendLinks.ts @@ -1,9 +1,10 @@ // Removed claimSendLink import - no longer used (was insecure) -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry, jsonParse, jsonStringify } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { jsonParse, jsonStringify } from '@/utils/general.utils' import { generateKeysFromString, getParamsFromLink } from '@squirrel-labs/peanut-sdk' import Cookies from 'js-cookie' import type { SendLink } from '@/services/services.types' +import { PEANUT_API_URL } from '@/constants/general.consts' export { ESendLinkStatus } from '@/services/services.types' export type { SendLinkStatus, SendLink } from '@/services/services.types' diff --git a/src/services/services.types.ts b/src/services/services.types.ts index 4bc1d036c..659bde792 100644 --- a/src/services/services.types.ts +++ b/src/services/services.types.ts @@ -1,4 +1,4 @@ -import { type BridgeKycStatus } from '@/utils' +import { type BridgeKycStatus } from '@/utils/bridge-accounts.utils' import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' export type TStatus = 'NEW' | 'PENDING' | 'COMPLETED' | 'EXPIRED' | 'FAILED' | 'SIGNED' | 'SUCCESSFUL' | 'CANCELLED' @@ -66,6 +66,7 @@ export interface ChargeEntry { } export interface RequestLink { + uuid: string recipientAddress: string reference: string | null attachmentUrl: string | null @@ -190,6 +191,7 @@ export interface TRequestChargeResponse { username: string } requestLink: { + uuid: string recipientAddress: string reference: string | null attachmentUrl: string | null @@ -469,3 +471,11 @@ export interface HistoryEntryPerkReward { originatingTxType?: string perkName?: string } + +export type RhinoChainType = 'EVM' | 'SOL' | 'TRON' +export interface CreateDepositAddressResponse { + depositAddress: string + minDepositLimitUsd: number + maxDepositLimitUsd: number + supportedChains: string[] +} diff --git a/src/services/simplefi.ts b/src/services/simplefi.ts index 7057171db..3c0eed0af 100644 --- a/src/services/simplefi.ts +++ b/src/services/simplefi.ts @@ -1,7 +1,7 @@ -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import Cookies from 'js-cookie' import type { Address } from 'viem' +import { PEANUT_API_URL } from '@/constants/general.consts' export type QrPaymentType = 'STATIC' | 'DYNAMIC' | 'USER_SPECIFIED' diff --git a/src/services/swap.ts b/src/services/swap.ts index 910fea268..21076defc 100644 --- a/src/services/swap.ts +++ b/src/services/swap.ts @@ -4,8 +4,10 @@ import { parseUnits, formatUnits, encodeFunctionData, erc20Abi } from 'viem' import { fetchTokenPrice, estimateTransactionCostUsd } from '@/app/actions/tokens' import { getPublicClient, type ChainId } from '@/app/actions/clients' -import { fetchWithSentry, isNativeCurrency, areEvmAddressesEqual } from '@/utils' -import { SQUID_API_URL, USDT_IN_MAINNET, SQUID_INTEGRATOR_ID, SQUID_INTEGRATOR_ID_WITHOUT_CORAL } from '@/constants' +import { fetchWithSentry } from '@/utils/sentry.utils' +import { isNativeCurrency, areEvmAddressesEqual } from '@/utils/general.utils' +import { SQUID_API_URL, SQUID_INTEGRATOR_ID, SQUID_INTEGRATOR_ID_WITHOUT_CORAL } from '@/constants/general.consts' +import { USDT_IN_MAINNET } from '@/constants/zerodev.consts' type TokenInfo = { address: Address diff --git a/src/services/users.ts b/src/services/users.ts index b485877c5..824acd885 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -1,17 +1,17 @@ import { - PEANUT_API_URL, PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN, PEANUT_WALLET_TOKEN_DECIMALS, PEANUT_WALLET_TOKEN_SYMBOL, -} from '@/constants' +} from '@/constants/zerodev.consts' import { AccountType, type IUserKycVerification } from '@/interfaces' -import { type IAttachmentOptions } from '@/redux/types/send-flow.types' -import { fetchWithSentry } from '@/utils' +import { type IAttachmentOptions } from '@/interfaces/attachment' +import { fetchWithSentry } from '@/utils/sentry.utils' import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' import { chargesApi } from './charges' import { type TCharge } from './services.types' import Cookies from 'js-cookie' +import { PEANUT_API_URL } from '@/constants/general.consts' type ApiAccount = { identifier: string diff --git a/src/styles/theme.ts b/src/styles/theme.ts deleted file mode 100644 index 311f8180f..000000000 --- a/src/styles/theme.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { extendTheme } from '@chakra-ui/react' -import { StepsTheme as Steps } from 'chakra-ui-steps' - -const config = { - initialColorMode: 'light' as 'light', - useSystemColorMode: false, -} -/** - * Breakpoints for responsive design. - * - * The breakpoints are defined in em units and their equivalent in pixels are: - * - xs: 22em (352px) - * - sm: 30em (480px) - * - md: 48em (768px) - * - lg: 62em (992px) - * - xl: 80em (1280px) - * - 2xl: 96em (1536px) - */ -export const breakpoints = { - xs: '22em', - sm: '30em', - md: '48em', - lg: '62em', - xl: '80em', - '2xl': '96em', -} - -export const emToPx = (em: string) => parseFloat(em) * 16 - -export const theme = extendTheme({ - breakpoints, - config, - colors: { - stepperScheme: { - 50: '#e1f5fe', - 100: '#b3e5fc', - 200: '#81d4fa', - 300: '#4fc3f7', - 400: '#29b6f6', - 500: '#03a9f4', - 600: '#039be5', - 700: '#0288d1', - 800: '#0277bd', - 900: '#01579b', - }, - pink: { - 500: '#FF9CEA', - }, - }, - - components: { - Steps, - }, -}) diff --git a/src/types/manteca.types.ts b/src/types/manteca.types.ts index 38040e779..d18d23f05 100644 --- a/src/types/manteca.types.ts +++ b/src/types/manteca.types.ts @@ -123,7 +123,8 @@ export type MantecaWithdrawResponse = { } export interface CreateMantecaOnrampParams { - usdAmount: string + amount: string + isUsdDenominated?: boolean currency: string chargeId?: string } diff --git a/src/utils/__mocks__/web-push.ts b/src/utils/__mocks__/web-push.ts deleted file mode 100644 index b0c44b7a0..000000000 --- a/src/utils/__mocks__/web-push.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Mock for web-push - * Used in Jest tests to avoid VAPID key validation issues - */ - -export const setVapidDetails = jest.fn() -export const sendNotification = jest.fn(() => Promise.resolve()) -export const generateVAPIDKeys = jest.fn(() => ({ - publicKey: 'mock-public-key', - privateKey: 'mock-private-key', -})) - -const webpush = { - setVapidDetails, - sendNotification, - generateVAPIDKeys, -} - -export default webpush diff --git a/src/utils/__tests__/balance.utils.test.ts b/src/utils/__tests__/balance.utils.test.ts new file mode 100644 index 000000000..00516f09a --- /dev/null +++ b/src/utils/__tests__/balance.utils.test.ts @@ -0,0 +1,29 @@ +import { printableUsdc } from '../balance.utils' + +describe('balance utils', () => { + describe('printableUsdc', () => { + it.each([ + [0n, '0.00'], + [10000n, '0.01'], + [100000n, '0.10'], + [1000000n, '1.00'], + [10000000n, '10.00'], + [100000000n, '100.00'], + [1000000000n, '1000.00'], + [10000000000n, '10000.00'], + [100000000000n, '100000.00'], + [1000000000000n, '1000000.00'], + [10000000000000n, '10000000.00'], + [100000000000000n, '100000000.00'], + [1000000000000000n, '1000000000.00'], + [10000000000000000n, '10000000000.00'], + [100000000000000000n, '100000000000.00'], + [1000000000000000000n, '1000000000000.00'], + [303340000n, '303.34'], + [303339000n, '303.33'], + [303345000n, '303.34'], + ])('should return the correct value for %i', (input, expected) => { + expect(printableUsdc(input)).toBe(expected) + }) + }) +}) diff --git a/src/utils/__tests__/url-parser.test.ts b/src/utils/__tests__/url-parser.test.ts index cdd19fe73..e26f64a73 100644 --- a/src/utils/__tests__/url-parser.test.ts +++ b/src/utils/__tests__/url-parser.test.ts @@ -84,16 +84,12 @@ jest.mock('@/app/actions/squid', () => ({ }), })) -jest.mock('@/constants', () => ({ +jest.mock('@/constants/zerodev.consts', () => ({ PEANUT_WALLET_CHAIN: { id: '42161', name: 'Arbitrum', }, PEANUT_WALLET_TOKEN: '0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8', - chains: [ - { id: 1, name: 'Ethereum' }, - { id: 42161, name: 'Arbitrum' }, - ], })) jest.mock('@/lib/url-parser/parser.consts', () => ({ @@ -267,7 +263,6 @@ describe('URL Parser Tests', () => { chain: expect.objectContaining({ chainId: 42161 }), amount: '0.1', token: expect.objectContaining({ symbol: 'USDC' }), - isDevConnectFlow: false, }) }) @@ -319,7 +314,6 @@ describe('URL Parser Tests', () => { chain: undefined, token: undefined, amount: undefined, - isDevConnectFlow: false, }) }) diff --git a/src/utils/backendTransport.ts b/src/utils/backendTransport.ts index c709da428..1a316f84b 100644 --- a/src/utils/backendTransport.ts +++ b/src/utils/backendTransport.ts @@ -1,7 +1,7 @@ import { custom, type Transport } from 'viem' -import { PEANUT_API_URL } from '@/constants' import { jsonStringify } from './general.utils' import Cookies from 'js-cookie' +import { PEANUT_API_URL } from '@/constants/general.consts' export function createBackendRpcTransport(chainId: number): Transport { return custom({ diff --git a/src/utils/balance.utils.ts b/src/utils/balance.utils.ts index 051ee3089..48c11a0f8 100644 --- a/src/utils/balance.utils.ts +++ b/src/utils/balance.utils.ts @@ -1,4 +1,4 @@ -import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants' +import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' import { type ChainValue, type IUserBalance } from '@/interfaces' import * as Sentry from '@sentry/nextjs' import { formatUnits } from 'viem' @@ -30,9 +30,10 @@ export function calculateValuePerChain(balances: IUserBalance[]): ChainValue[] { } export const printableUsdc = (balance: bigint): string => { - const formatted = formatUnits(balance, PEANUT_WALLET_TOKEN_DECIMALS) - // floor the formatted value - const value = Number(formatted) - const flooredValue = Math.floor(value * 100) / 100 - return flooredValue.toFixed(2) + // For 6 decimals, we want 2 decimal places in output + // So we divide by 10^4 to keep only 2 decimal places, then format + const scaleFactor = BigInt(10 ** (PEANUT_WALLET_TOKEN_DECIMALS - 2)) // 10^4 = 10000n + const flooredBigint = (balance / scaleFactor) * scaleFactor + const formatted = formatUnits(flooredBigint, PEANUT_WALLET_TOKEN_DECIMALS) + return Number(formatted).toFixed(2) } diff --git a/src/utils/bridge-accounts.utils.ts b/src/utils/bridge-accounts.utils.ts index 0c24a2ed7..61bd013c6 100644 --- a/src/utils/bridge-accounts.utils.ts +++ b/src/utils/bridge-accounts.utils.ts @@ -1,5 +1,6 @@ -import * as consts from '@/constants' -import { areEvmAddressesEqual, fetchWithSentry } from '@/utils' +import { supportedBridgeTokensDictionary, supportedBridgeChainsDictionary } from '@/constants/cashout.consts' +import { areEvmAddressesEqual } from '@/utils/general.utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import { isIBAN } from 'validator' const ALLOWED_PARENT_DOMAINS = ['intersend.io', 'app.intersend.io'] @@ -31,14 +32,15 @@ export const convertPersonaUrl = (url: string) => { return `https://bridge.withpersona.com/widget?environment=production&inquiry-template-id=${templateId}&fields[iqt_token=${iqtToken}&iframe-origin=${origin}&redirect-uri=${origin}&fields[developer_id]=${developerId}&reference-id=${referenceId}` } -export type BridgeKycStatus = 'not_started' | 'under_review' | 'approved' | 'rejected' | 'incomplete' +// Re-export from interfaces (defined there to avoid circular dependency) +export type { BridgeKycStatus } from '@/interfaces/interfaces' export async function validateIban(iban: string): Promise { return isIBAN(iban.replace(/\s+/g, '')) } export function getBridgeTokenName(chainId: string, tokenAddress: string): string | undefined { - const token = consts.supportedBridgeTokensDictionary + const token = supportedBridgeTokensDictionary .find((chain) => chain.chainId === chainId) ?.tokens.find((token) => areEvmAddressesEqual(token.address, tokenAddress)) ?.token.toLowerCase() @@ -47,7 +49,7 @@ export function getBridgeTokenName(chainId: string, tokenAddress: string): strin } export function getBridgeChainName(chainId: string): string | undefined { - const chain = consts.supportedBridgeChainsDictionary.find((chain) => chain.chainId === chainId)?.chain + const chain = supportedBridgeChainsDictionary.find((chain) => chain.chainId === chainId)?.chain return chain ?? undefined } diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts index f13c7a6ea..afb85130a 100644 --- a/src/utils/general.utils.ts +++ b/src/utils/general.utils.ts @@ -1,6 +1,13 @@ -import * as consts from '@/constants' -import { STABLE_COINS, USER_OPERATION_REVERT_REASON_TOPIC, ENS_NAME_REGEX } from '@/constants' -import { AccountType } from '@/interfaces' +import { + nativeCurrencyAddresses, + supportedPeanutChains, + peanutTokenDetails, + pathTitles, + BASE_URL, +} from '@/constants/general.consts' +import { type LoadingStates } from '@/constants/loadingStates.consts' +import { STABLE_COINS, ENS_NAME_REGEX } from '@/constants/general.consts' +import { AccountType } from '@/interfaces/interfaces' import * as Sentry from '@sentry/nextjs' import peanut, { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' import type { Address, TransactionReceipt } from 'viem' @@ -10,7 +17,8 @@ import { getPublicClient, type ChainId } from '@/app/actions/clients' import { NATIVE_TOKEN_ADDRESS, SQUID_ETH_ADDRESS } from './token.utils' import { type ChargeEntry } from '@/services/services.types' import { toWebAuthnKey } from '@zerodev/passkey-validator' -import type { ParsedURL } from '@/lib/url-parser/types/payment' +import { USER_OPERATION_REVERT_REASON_TOPIC } from '@/constants/zerodev.consts' +import { CHAIN_LOGOS, type ChainName } from '@/constants/rhino.consts' export function urlBase64ToUint8Array(base64String: string) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4) @@ -54,8 +62,36 @@ export const shortenStringLong = (s?: string, chars?: number, firstChars?: numbe return firstBit + '...' + endingBit } +// Address detection patterns (permissive to handle lowercase-stored addresses) +// These are for display purposes, not cryptographic validation +const SOLANA_ADDRESS_REGEX = /^[1-9a-zA-Z]{32,44}$/ +const TRON_ADDRESS_REGEX = /^[Tt][0-9a-zA-Z]{33}$/ + +/** + * Checks if a string looks like a Solana address (32-44 alphanumeric characters, no 0) + * Permissive to handle lowercase-stored addresses + */ +export const isSolanaAddress = (address: string): boolean => { + return SOLANA_ADDRESS_REGEX.test(address) +} + +/** + * Checks if a string looks like a Tron address (starts with T/t, 34 alphanumeric characters) + * Permissive to handle lowercase-stored addresses + */ +export const isTronAddress = (address: string): boolean => { + return TRON_ADDRESS_REGEX.test(address) +} + +/** + * Checks if a string is any valid blockchain address (EVM, Solana, or Tron) + */ +export const isCryptoAddress = (address: string): boolean => { + return isAddress(address) || isSolanaAddress(address) || isTronAddress(address) +} + export const printableAddress = (address: string, firstCharsLen?: number, lastCharsLen?: number): string => { - if (!isAddress(address)) return address + if (!isCryptoAddress(address)) return address return shortenStringLong(address, undefined, firstCharsLen, lastCharsLen) } @@ -434,7 +470,7 @@ export const isAddressZero = (address: string): boolean => { } export const isNativeCurrency = (address: string) => { - if (consts.nativeCurrencyAddresses.includes(address.toLowerCase())) { + if (nativeCurrencyAddresses.includes(address.toLowerCase())) { return true } else return false } @@ -456,16 +492,6 @@ export type UserPreferences = { notifBannerShowAt?: number notifModalClosed?: boolean hasSeenBalanceWarning?: { value: boolean; expiry: number } - // @dev: note, this needs to be deleted post devconnect - devConnectIntents?: Array<{ - id: string - recipientAddress: string - chain: string - amount: string - onrampId?: string - createdAt: number - status: 'pending' | 'completed' - }> } export const updateUserPreferences = ( @@ -511,7 +537,7 @@ export const estimateIfIsStableCoinFromPrice = (tokenPrice: number) => { } export const getExplorerUrl = (chainId: string) => { - const explorers = consts.supportedPeanutChains.find((detail) => detail.chainId === chainId)?.explorers + const explorers = supportedPeanutChains.find((detail) => detail.chainId === chainId)?.explorers // if the explorers array has blockscout, return the blockscout url, else return the first one if (explorers?.find((explorer) => explorer.url.includes('blockscout'))) { return explorers?.find((explorer) => explorer.url.includes('blockscout'))?.url @@ -565,7 +591,7 @@ export const switchNetwork = async ({ }: { chainId: string currentChainId: string | undefined - setLoadingState: (state: consts.LoadingStates) => void + setLoadingState: (state: LoadingStates) => void switchChainAsync: ({ chainId }: { chainId: number }) => Promise }) => { if (currentChainId !== chainId) { @@ -585,7 +611,7 @@ export const switchNetwork = async ({ /** Gets the token decimals for a given token address and chain ID. */ export function getTokenDecimals(tokenAddress: string, chainId: string): number | undefined { - return consts.peanutTokenDetails + return peanutTokenDetails .find((chain) => chain.chainId === chainId) ?.tokens.find((token) => areEvmAddressesEqual(token.address, tokenAddress))?.decimals } @@ -597,7 +623,7 @@ export function getTokenDetails({ tokenAddress, chainId }: { tokenAddress: Addre decimals: number } | undefined { - const chainTokens = consts.peanutTokenDetails.find((c) => c.chainId === chainId)?.tokens + const chainTokens = peanutTokenDetails.find((c) => c.chainId === chainId)?.tokens if (!chainTokens) return undefined const tokenDetails = chainTokens.find((token) => areEvmAddressesEqual(token.address, tokenAddress)) if (!tokenDetails) return undefined @@ -615,7 +641,7 @@ export function getTokenDetails({ tokenAddress, chainId }: { tokenAddress: Addre export function getTokenSymbol(tokenAddress: string | undefined, chainId: string | undefined): string | undefined { if (!tokenAddress || !chainId) return undefined - const chainTokens = consts.peanutTokenDetails.find((chain) => chain.chainId === chainId)?.tokens + const chainTokens = peanutTokenDetails.find((chain) => chain.chainId === chainId)?.tokens if (!chainTokens) return undefined return chainTokens.find((token) => areEvmAddressesEqual(token.address, tokenAddress))?.symbol @@ -654,12 +680,15 @@ export async function fetchTokenSymbol(tokenAddress: string, chainId: string): P } export function getChainName(chainId: string): string | undefined { + if (chainId === '0') { + return 'Solana' + } const chain = Object.entries(wagmiChains).find(([, chain]) => chain.id === Number(chainId))?.[1] return chain?.name ?? undefined } export const getHeaderTitle = (pathname: string) => { - return consts.pathTitles[pathname] || 'Peanut' // default title if path not found + return pathTitles[pathname] || 'Peanut' // default title if path not found } /** @@ -780,6 +809,13 @@ export function getChainLogo(chainName: string): string { default: name = chainName.toLowerCase() } + + const chainLogo = CHAIN_LOGOS[name.toUpperCase() as ChainName] + + if (chainLogo) { + return chainLogo + } + return `https://raw.githubusercontent.com/0xsquid/assets/main/images/webp128/chains/${name}.webp` } @@ -896,7 +932,7 @@ export const generateInviteCodeSuffix = (username: string): string => { export const generateInviteCodeLink = (username: string) => { const suffix = generateInviteCodeSuffix(username) const inviteCode = `${username.toUpperCase()}INVITESYOU${suffix}` - const inviteLink = `${consts.BASE_URL}/invite?code=${inviteCode}` + const inviteLink = `${BASE_URL}/invite?code=${inviteCode}` return { inviteLink, inviteCode } } @@ -939,109 +975,3 @@ export const getContributorsFromCharge = (charges: ChargeEntry[]) => { } }) } - -/** - * helper function to save devconnect intent to user preferences - * @dev: note, this needs to be deleted post devconnect - */ -/** - * create deterministic id for devconnect intent based on recipient + chain only - * amount is not included as it can change during the flow - * @dev: to be deleted post devconnect - */ -const createDevConnectIntentId = (recipientAddress: string, chain: string): string => { - const str = `${recipientAddress.toLowerCase()}-${chain.toLowerCase()}` - let hash = 0 - for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i) - hash = (hash << 5) - hash + char - hash = hash & hash // convert to 32bit integer - } - return Math.abs(hash).toString(36) -} - -export const saveDevConnectIntent = ( - userId: string | undefined, - parsedPaymentData: ParsedURL | null, - amount: string, - onrampId?: string -): void => { - if (!userId) return - - // check both redux state and user preferences (fallback if state was reset) - const devconnectFlowData = - parsedPaymentData?.isDevConnectFlow && parsedPaymentData.recipient && parsedPaymentData.chain - ? { - recipientAddress: parsedPaymentData.recipient.resolvedAddress, - chain: parsedPaymentData.chain.chainId, - } - : (() => { - try { - const prefs = getUserPreferences(userId) - const intents = prefs?.devConnectIntents ?? [] - // get the most recent pending intent - return intents.find((i) => i.status === 'pending') ?? null - } catch (e) { - console.error('Failed to read devconnect intent from user preferences:', e) - } - return null - })() - - if (devconnectFlowData) { - // validate required fields - const recipientAddress = devconnectFlowData.recipientAddress - const chain = devconnectFlowData.chain - const cleanedAmount = amount.replace(/,/g, '') - - if (!recipientAddress || !chain || !cleanedAmount) { - console.warn('Skipping DevConnect intent: missing required fields') - return - } - - try { - // create deterministic id based on address + chain only - const intentId = createDevConnectIntentId(recipientAddress, chain) - - const prefs = getUserPreferences(userId) - const existingIntents = prefs?.devConnectIntents ?? [] - - // check if intent with same id already exists - const existingIntent = existingIntents.find((intent) => intent.id === intentId) - - if (!existingIntent) { - // create new intent - const { MAX_DEVCONNECT_INTENTS } = require('@/constants/payment.consts') - const sortedIntents = existingIntents.sort((a, b) => b.createdAt - a.createdAt) - const prunedIntents = sortedIntents.slice(0, MAX_DEVCONNECT_INTENTS - 1) - - updateUserPreferences(userId, { - devConnectIntents: [ - { - id: intentId, - recipientAddress, - chain, - amount: cleanedAmount, - onrampId, - createdAt: Date.now(), - status: 'pending', - }, - ...prunedIntents, - ], - }) - } else { - // update existing intent with new amount and onrampId - const updatedIntents = existingIntents.map((intent) => - intent.id === intentId - ? { ...intent, amount: cleanedAmount, onrampId, createdAt: Date.now() } - : intent - ) - updateUserPreferences(userId, { - devConnectIntents: updatedIntents, - }) - } - } catch (intentError) { - console.error('Failed to save DevConnect intent:', intentError) - // don't block the flow if intent storage fails - } - } -} diff --git a/src/utils/history.utils.ts b/src/utils/history.utils.ts index 5f81f18d0..b233e4f5b 100644 --- a/src/utils/history.utils.ts +++ b/src/utils/history.utils.ts @@ -1,13 +1,15 @@ import { MERCADO_PAGO, PIX, SIMPLEFI } from '@/assets/payment-apps' import { type TransactionDetails } from '@/components/TransactionDetails/transactionTransformer' -import { getFromLocalStorage } from '@/utils' -import { PEANUT_WALLET_TOKEN_DECIMALS, BASE_URL } from '@/constants' +import { getFromLocalStorage } from '@/utils/general.utils' import { formatUnits } from 'viem' import { type Hash } from 'viem' -import { getTokenDetails } from '@/utils' +import { getTokenDetails } from '@/utils/general.utils' import { getCurrencyPrice } from '@/app/actions/currency' import { type ChargeEntry } from '@/services/services.types' +import { BASE_URL } from '@/constants/general.consts' +import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' +// NOTE: do not change the order, add new entries at the end, keep synced with backend export enum EHistoryEntryType { REQUEST = 'REQUEST', CASHOUT = 'CASHOUT', @@ -19,10 +21,10 @@ export enum EHistoryEntryType { BRIDGE_ONRAMP = 'BRIDGE_ONRAMP', BANK_SEND_LINK_CLAIM = 'BANK_SEND_LINK_CLAIM', MANTECA_QR_PAYMENT = 'MANTECA_QR_PAYMENT', - SIMPLEFI_QR_PAYMENT = 'SIMPLEFI_QR_PAYMENT', MANTECA_OFFRAMP = 'MANTECA_OFFRAMP', MANTECA_ONRAMP = 'MANTECA_ONRAMP', BRIDGE_GUEST_OFFRAMP = 'BRIDGE_GUEST_OFFRAMP', + SIMPLEFI_QR_PAYMENT = 'SIMPLEFI_QR_PAYMENT', PERK_REWARD = 'PERK_REWARD', } export function historyTypeToNumber(type: EHistoryEntryType): number { diff --git a/src/utils/identityVerification.tsx b/src/utils/identityVerification.tsx index 3f6abb80f..d4e1a3f93 100644 --- a/src/utils/identityVerification.tsx +++ b/src/utils/identityVerification.tsx @@ -1,22 +1,32 @@ -import { ALL_COUNTRIES_ALPHA3_TO_ALPHA2, countryData, MEXICO_ALPHA3_TO_ALPHA2 } from '@/components/AddMoney/consts' +import { + ALL_COUNTRIES_ALPHA3_TO_ALPHA2, + countryData, + MEXICO_ALPHA3_TO_ALPHA2, + UNSUPPORTED_ALPHA3_TO_ALPHA2, +} from '@/components/AddMoney/consts' export const getCountriesForRegion = (region: string) => { const supportedCountriesIso3 = Object.keys(ALL_COUNTRIES_ALPHA3_TO_ALPHA2).concat( Object.keys(MEXICO_ALPHA3_TO_ALPHA2) // Add Mexico as well, supported by bridge ) + const unsupportedCountriesIso3 = Object.keys(UNSUPPORTED_ALPHA3_TO_ALPHA2) + const countries = countryData.filter((country) => country.region === region) const supportedCountries = [] + const limitedAccessCountries = [] const unsupportedCountries = [] for (const country of countries) { if (country.iso3 && supportedCountriesIso3.includes(country.iso3)) { supportedCountries.push({ ...country, isSupported: true }) - } else { + } else if (country.iso3 && unsupportedCountriesIso3.includes(country.iso3)) { unsupportedCountries.push({ ...country, isSupported: false }) + } else { + limitedAccessCountries.push({ ...country, isSupported: false }) } } - return { supportedCountries, unsupportedCountries } + return { supportedCountries, limitedAccessCountries, unsupportedCountries } } diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index 5ea9ac668..000000000 --- a/src/utils/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -export * from './general.utils' -export * from './sdkErrorHandler.utils' -export * from './bridge-accounts.utils' -export * from './balance.utils' -export * from './sentry.utils' -export * from './token.utils' -export * from './ens.utils' -export * from './history.utils' -export * from './auth.utils' -export * from './webauthn.utils' -export * from './passkeyDebug' -export * from './passkeyPreflight' - -// Bridge utils - explicit exports to avoid naming conflicts -export { - getCurrencyConfig as getBridgeCurrencyConfig, - getOfframpCurrencyConfig, - getPaymentRailDisplayName, - getMinimumAmount, -} from './bridge.utils' -export type { BridgeOperationType } from './bridge.utils' diff --git a/src/utils/metrics.utils.ts b/src/utils/metrics.utils.ts index 390991bc2..9edbc8290 100644 --- a/src/utils/metrics.utils.ts +++ b/src/utils/metrics.utils.ts @@ -1,7 +1,7 @@ import { type JSONObject } from '@/interfaces' -import { PEANUT_API_URL } from '@/constants' -import { fetchWithSentry } from '@/utils' +import { fetchWithSentry } from '@/utils/sentry.utils' import Cookies from 'js-cookie' +import { PEANUT_API_URL } from '@/constants/general.consts' export async function hitUserMetric(userId: string, name: string, value: JSONObject = {}): Promise { try { diff --git a/src/utils/token.utils.ts b/src/utils/token.utils.ts index ffbd6da4b..7453dea6d 100644 --- a/src/utils/token.utils.ts +++ b/src/utils/token.utils.ts @@ -1,4 +1,4 @@ -import { areEvmAddressesEqual } from '@/utils/' +import { areEvmAddressesEqual } from './general.utils' export const checkTokenSupportsXChain = ( tokenAddress: string, diff --git a/tailwind.config.js b/tailwind.config.js index 55292eab7..dfa378f6d 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -100,6 +100,8 @@ module.exports = { 3: '#29CC6A', 4: '#1C6A50', 5: '#88D987', + 6: '#ECFFE9', + 7: '#4B8A17', }, white: '#FFFFFF', red: '#FF0000',