diff --git a/.env.example b/.env.example index c5f0ec770..af6c639a9 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,7 @@ export NEXT_PUBLIC_WC_PROJECT_ID="" export NEXT_PUBLIC_SENTRY_DSN="" export NEXT_PUBLIC_RECAPTCHA_SITE_KEY="" export NEXT_PUBLIC_INFURA_API_KEY="" +export NEXT_PUBLIC_ALCHEMY_API_KEY="" export NEXT_PUBLIC_JUSTANAME_ENS_DOMAIN="" export MOBULA_API_KEY="" @@ -26,6 +27,10 @@ export WC_PROJECT_ID="" export GA_KEY="" export SOCKET_API_KEY="" +# SQUID +export SQUID_API_URL="" +export SQUID_INTEGRATOR_ID="" + # Passkey envs export NEXT_PUBLIC_ZERO_DEV_PASSKEY_PROJECT_ID="" export NEXT_PUBLIC_ZERO_DEV_BUNDLER_URL="" @@ -33,6 +38,8 @@ export NEXT_PUBLIC_ZERO_DEV_PAYMASTER_URL="" export NEXT_PUBLIC_ZERO_DEV_PASSKEY_SERVER_URL="" export NEXT_PUBLIC_POLYGON_PAYMASTER_URL="" export NEXT_PUBLIC_POLYGON_BUNDLER_URL="" +export NEXT_PUBLIC_BALANCE_WARNING_THRESHOLD=1 +export NEXT_PUBLIC_BALANCE_WARNING_EXPIRY=15 export NEXT_PUBLIC_INFURA_API_KEY="" diff --git a/package.json b/package.json index 3af682a87..a2ea4c505 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@hookform/resolvers": "3.9.1", "@justaname.id/react": "0.3.180", "@justaname.id/sdk": "0.2.177", + "@radix-ui/react-slider": "^1.3.5", "@reduxjs/toolkit": "^2.5.0", "@reown/appkit": "1.6.9", "@reown/appkit-adapter-wagmi": "1.6.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef1c783c8..62fbdd5d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,6 +52,9 @@ importers: '@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.22.11(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)(zod@3.24.1)) + '@radix-ui/react-slider': + specifier: ^1.3.5 + version: 1.3.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@reduxjs/toolkit': specifier: ^2.5.0 version: 2.5.0(react-redux@9.2.0(@types/react@18.3.18)(react@19.1.0)(redux@5.0.1))(react@19.1.0) @@ -2406,9 +2409,25 @@ packages: engines: {node: '>=18'} hasBin: true + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + 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-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -2440,6 +2459,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dismissable-layer@1.1.10': resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==} peerDependencies: @@ -2523,6 +2551,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-slider@1.3.5': + resolution: {integrity: sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==} + 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-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: @@ -2577,6 +2618,24 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@react-native/assets-registry@0.77.0': resolution: {integrity: sha512-Ms4tYYAMScgINAXIhE4riCFJPPL/yltughHS950l0VP5sm5glbimn9n7RFn9Tc8cipX74/ddbk19+ydK2iDMmA==} engines: {node: '>=18'} @@ -10002,7 +10061,7 @@ snapshots: '@types/debug': 4.1.12 debug: 4.4.0 pony-cause: 2.1.11 - semver: 7.6.3 + semver: 7.7.1 uuid: 9.0.1 transitivePeerDependencies: - supports-color @@ -10476,8 +10535,22 @@ snapshots: - bare-buffer - supports-color + '@radix-ui/number@1.1.1': {} + '@radix-ui/primitive@1.1.2': {} + '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.18)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.18)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.18)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.18)(react@19.1.0)': dependencies: react: 19.1.0 @@ -10512,6 +10585,12 @@ snapshots: '@types/react': 18.3.18 '@types/react-dom': 18.3.5(@types/react@18.3.18) + '@radix-ui/react-direction@1.1.1(@types/react@18.3.18)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 18.3.18 + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -10578,6 +10657,25 @@ snapshots: '@types/react': 18.3.18 '@types/react-dom': 18.3.5(@types/react@18.3.18) + '@radix-ui/react-slider@1.3.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.18)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.18)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.18)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.18)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.18)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.18)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.18)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + '@radix-ui/react-slot@1.2.3(@types/react@18.3.18)(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.18)(react@19.1.0) @@ -10619,6 +10717,19 @@ snapshots: optionalDependencies: '@types/react': 18.3.18 + '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.18)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-use-size@1.1.1(@types/react@18.3.18)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.18)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 18.3.18 + '@react-native/assets-registry@0.77.0': {} '@react-native/babel-plugin-codegen@0.77.0(@babel/preset-env@7.26.0(@babel/core@7.26.0))': @@ -15096,7 +15207,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.6.3 + semver: 7.7.1 make-error@1.3.6: {} diff --git a/public/arrows/small-arrow.svg b/public/arrows/small-arrow.svg new file mode 100644 index 000000000..2d958eedf --- /dev/null +++ b/public/arrows/small-arrow.svg @@ -0,0 +1,4 @@ + + + + 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 8697156a5..f7d191152 100644 --- a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx +++ b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx @@ -38,7 +38,7 @@ export default function OnrampBankPage() { const [isKycModalOpen, setIsKycModalOpen] = useState(false) const [liveKycStatus, setLiveKycStatus] = useState(undefined) - const { amountToOnramp: amountFromContext, setAmountToOnramp, setError, error } = useOnrampFlow() + const { amountToOnramp: amountFromContext, setAmountToOnramp, setError, error, setOnrampData } = useOnrampFlow() const formRef = useRef<{ handleSubmit: () => void }>(null) const [isUpdatingUser, setIsUpdatingUser] = useState(false) const [userUpdateError, setUserUpdateError] = useState(null) @@ -186,7 +186,7 @@ export default function OnrampBankPage() { amount: cleanedAmount, country: selectedCountry, }) - sessionStorage.setItem('onrampData', JSON.stringify(onrampDataResponse)) + setOnrampData(onrampDataResponse) if (onrampDataResponse.transferId) { setStep('showDetails') diff --git a/src/app/(mobile-ui)/home/page.tsx b/src/app/(mobile-ui)/home/page.tsx index 78178bcc8..fa8e67858 100644 --- a/src/app/(mobile-ui)/home/page.tsx +++ b/src/app/(mobile-ui)/home/page.tsx @@ -16,7 +16,14 @@ import { UserHeader } from '@/components/UserHeader' import { useAuth } from '@/context/authContext' import { useWallet } from '@/hooks/wallet/useWallet' import { useUserStore, useWalletStore } from '@/redux/hooks' -import { formatExtendedNumber, getUserPreferences, printableUsdc, updateUserPreferences } from '@/utils' +import { + formatExtendedNumber, + getUserPreferences, + printableUsdc, + updateUserPreferences, + getFromLocalStorage, + saveToLocalStorage, +} from '@/utils' import { useDisconnect } from '@reown/appkit/react' import Image from 'next/image' import Link from 'next/link' @@ -24,7 +31,13 @@ import { useEffect, useMemo, useState } from 'react' import { twMerge } from 'tailwind-merge' import { useAccount } from 'wagmi' import AddMoneyPromptModal from '@/components/Home/AddMoneyPromptModal' +import BalanceWarningModal from '@/components/Global/BalanceWarningModal' import { AccountType } from '@/interfaces' +import { formatUnits } from 'viem' +import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants' + +const BALANCE_WARNING_THRESHOLD = parseInt(process.env.NEXT_PUBLIC_BALANCE_WARNING_THRESHOLD ?? '500') +const BALANCE_WARNING_EXPIRY = parseInt(process.env.NEXT_PUBLIC_BALANCE_WARNING_EXPIRY ?? '1814400') // 21 days in seconds export default function Home() { const { balance, address, isFetchingBalance, isFetchingRewardBalance } = useWallet() @@ -43,6 +56,7 @@ export default function Home() { const [showIOSPWAInstallModal, setShowIOSPWAInstallModal] = useState(false) const [showAddMoneyPromptModal, setShowAddMoneyPromptModal] = useState(false) + const [showBalanceWarningModal, setShowBalanceWarningModal] = useState(false) const userFullName = useMemo(() => { if (!user) return @@ -97,6 +111,27 @@ export default function Home() { } }, [user?.hasPwaInstalled]) + // effect for showing balance warning modal + useEffect(() => { + if (typeof window !== 'undefined' && !isFetchingBalance) { + const hasSeenBalanceWarning = getFromLocalStorage('hasSeenBalanceWarning') + const balanceInUsd = Number(formatUnits(balance, PEANUT_WALLET_TOKEN_DECIMALS)) + + // show if: + // 1. balance is above the threshold + // 2. user hasn't seen this warning in the current session + // 3. no other modals are currently active + if ( + balanceInUsd > BALANCE_WARNING_THRESHOLD && + !hasSeenBalanceWarning && + !showIOSPWAInstallModal && + !showAddMoneyPromptModal + ) { + setShowBalanceWarningModal(true) + } + } + }, [balance, isFetchingBalance, showIOSPWAInstallModal, showAddMoneyPromptModal]) + // effect for showing add money prompt modal useEffect(() => { if (typeof window !== 'undefined' && !isFetchingBalance) { @@ -106,14 +141,20 @@ export default function Home() { // 1. balance is zero. // 2. user hasn't seen this prompt in the current session. // 3. the iOS PWA install modal is not currently active. + // 4. the balance warning modal is not currently active. // this allows the modal on any device (iOS/Android) and in any display mode (PWA/browser), // as long as the PWA modal (which is iOS & browser-specific) isn't taking precedence. - if (balance === 0n && !hasSeenAddMoneyPromptThisSession && !showIOSPWAInstallModal) { + if ( + balance === 0n && + !hasSeenAddMoneyPromptThisSession && + !showIOSPWAInstallModal && + !showBalanceWarningModal + ) { setShowAddMoneyPromptModal(true) sessionStorage.setItem('hasSeenAddMoneyPromptThisSession', 'true') } } - }, [balance, isFetchingBalance, showIOSPWAInstallModal]) + }, [balance, isFetchingBalance, showIOSPWAInstallModal, showBalanceWarningModal]) if (isLoading) { return @@ -174,6 +215,15 @@ export default function Home() { {/* Add Money Prompt Modal */} setShowAddMoneyPromptModal(false)} /> + + {/* Balance Warning Modal */} + { + setShowBalanceWarningModal(false) + saveToLocalStorage('hasSeenBalanceWarning', 'true', BALANCE_WARNING_EXPIRY) + }} + /> ) } diff --git a/src/app/(mobile-ui)/layout.tsx b/src/app/(mobile-ui)/layout.tsx index 771495bdb..29a5238c1 100644 --- a/src/app/(mobile-ui)/layout.tsx +++ b/src/app/(mobile-ui)/layout.tsx @@ -1,6 +1,9 @@ 'use client' -import { GenericBanner } from '@/components/Global/Banner' +import { MarqueeWrapper } from '@/components/Global/MarqueeWrapper' +import { useRouter } from 'next/navigation' +import { HandThumbsUp } from '@/assets' +import Image from 'next/image' import GuestLoginModal from '@/components/Global/GuestLoginModal' import PeanutLoading from '@/components/Global/PeanutLoading' import TopNavbar from '@/components/Global/TopNavbar' @@ -22,6 +25,7 @@ const publicPathRegex = /^\/(request\/pay|claim|pay\/.+$)/ const Layout = ({ children }: { children: React.ReactNode }) => { const pathName = usePathname() + const router = useRouter() const { isFetchingUser, user } = useAuth() const [isReady, setIsReady] = useState(false) const [hasToken, setHasToken] = useState(false) @@ -99,10 +103,17 @@ const Layout = ({ children }: { children: React.ReactNode }) => { {/* Main content area */}
- + {/* Only show banner if not on landing page */} + {pathName !== '/' && ( + + )} {/* Fixed top navbar */} {showFullPeanutWallet && (
diff --git a/src/app/(mobile-ui)/request/create/page.tsx b/src/app/(mobile-ui)/request/create/page.tsx index 73befa76a..66563a0e2 100644 --- a/src/app/(mobile-ui)/request/create/page.tsx +++ b/src/app/(mobile-ui)/request/create/page.tsx @@ -11,7 +11,7 @@ export const metadata = generateMetadata({ export default function RequestCreate() { return ( - + ) diff --git a/src/app/(mobile-ui)/send/page.tsx b/src/app/(mobile-ui)/send/page.tsx index b13f9c7a3..7656d7f23 100644 --- a/src/app/(mobile-ui)/send/page.tsx +++ b/src/app/(mobile-ui)/send/page.tsx @@ -12,7 +12,7 @@ export const metadata = generateMetadata({ export default function SendPage() { return ( - + ) diff --git a/src/app/(mobile-ui)/support/page.tsx b/src/app/(mobile-ui)/support/page.tsx index 0fd00a6cd..d9ec886cf 100644 --- a/src/app/(mobile-ui)/support/page.tsx +++ b/src/app/(mobile-ui)/support/page.tsx @@ -4,7 +4,7 @@ const SupportPage = () => { return (