Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ 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_INFURA_API_KEY=""

Expand Down
44 changes: 42 additions & 2 deletions src/app/(mobile-ui)/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ 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')

export default function Home() {
const { balance, address, isFetchingBalance, isFetchingRewardBalance } = useWallet()
Expand All @@ -43,6 +48,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
Expand Down Expand Up @@ -97,6 +103,28 @@ export default function Home() {
}
}, [user?.hasPwaInstalled])

// effect for showing balance warning modal
useEffect(() => {
if (typeof window !== 'undefined' && !isFetchingBalance) {
const hasSeenBalanceWarningThisSession = sessionStorage.getItem('hasSeenBalanceWarningThisSession')
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 &&
!hasSeenBalanceWarningThisSession &&
!showIOSPWAInstallModal &&
!showAddMoneyPromptModal
) {
setShowBalanceWarningModal(true)
sessionStorage.setItem('hasSeenBalanceWarningThisSession', 'true')
}
}
}, [balance, isFetchingBalance, showIOSPWAInstallModal, showAddMoneyPromptModal])

// effect for showing add money prompt modal
useEffect(() => {
if (typeof window !== 'undefined' && !isFetchingBalance) {
Expand All @@ -106,14 +134,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 <PeanutLoading coverFullScreen />
Expand Down Expand Up @@ -174,6 +208,12 @@ export default function Home() {

{/* Add Money Prompt Modal */}
<AddMoneyPromptModal visible={showAddMoneyPromptModal} onClose={() => setShowAddMoneyPromptModal(false)} />

{/* Balance Warning Modal */}
<BalanceWarningModal
visible={showBalanceWarningModal}
onCloseAction={() => setShowBalanceWarningModal(false)}
/>
</PageContainer>
)
}
Expand Down
21 changes: 16 additions & 5 deletions src/app/(mobile-ui)/layout.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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)
Expand Down Expand Up @@ -99,10 +103,17 @@ const Layout = ({ children }: { children: React.ReactNode }) => {

{/* Main content area */}
<div className="flex w-full flex-1 flex-col">
<GenericBanner
message="Alpha version: Report anything weird to support and enjoy our bug bounty!"
backgroundColor="bg-primary-1"
/>
{/* Only show banner if not on landing page */}
{pathName !== '/' && (
<button onClick={() => router.push('/support')} className="w-full cursor-pointer">
<MarqueeWrapper backgroundColor="bg-primary-1" direction="left">
<span className="z-10 mx-4 flex items-center gap-2 text-sm font-semibold">
Peanut is in beta! Thank you for being an early user, share your feedback here
<Image src={HandThumbsUp} alt="Thumbs up" className="h-4 w-4" />
</span>
</MarqueeWrapper>
</button>
)}
{/* Fixed top navbar */}
{showFullPeanutWallet && (
<div className="sticky top-0 z-10 w-full">
Expand Down
2 changes: 1 addition & 1 deletion src/app/(mobile-ui)/support/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const SupportPage = () => {
return (
<iframe
src="https://go.crisp.chat/chat/embed/?website_id=916078be-a6af-4696-82cb-bc08d43d9125"
className="h-full w-full"
className="h-full w-full md:max-w-[90%] md:pl-24"
/>
)
}
Expand Down
104 changes: 103 additions & 1 deletion src/app/actions/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { type ITokenPriceData } from '@/interfaces'
import type { Address } from 'viem'
import { parseAbi } from 'viem'
import { type ChainId, getPublicClient } from '@/app/actions/clients'
import { getTokenDetails } from '@/utils'
import { getTokenDetails, isStableCoin, areEvmAddressesEqual } from '@/utils'
import { IUserBalance } from '@/interfaces'

type IMobulaMarketData = {
id: number
Expand Down Expand Up @@ -50,6 +51,49 @@ type IMobulaMarketData = {
}
}

type IMobulaContractBalanceData = {
address: string //of the contract
balance: number
balanceRaw: string
chainId: string // this chainId is og the type evm:<chainId>
decimals: number
}

type IMobulaCrossChainBalanceData = {
balance: number
balanceRaw: string
chainId: string
address: string //of the token
}

type IMobulaAsset = {
id: number
name: string
symbol: string
logo: string
decimals: string[]
contracts: string[]
blockchains: string[]
}

type IMobulaAssetData = {
contracts_balances: IMobulaContractBalanceData[]
cross_chain_balances: Record<string, IMobulaCrossChainBalanceData> // key is the same as in asset.blockchains price_change_24h: number
estimated_balance: number
price: number
token_balance: number
allocation: number
asset: IMobulaAsset
wallets: string[]
}

type IMobulaPortfolioData = {
total_wallet_balance: number
wallets: string[]
assets: IMobulaAssetData[]
balances_length: number
}

const ERC20_DATA_ABI = parseAbi([
'function symbol() view returns (string)',
'function name() view returns (string)',
Expand Down Expand Up @@ -146,3 +190,61 @@ export const fetchTokenDetails = unstable_cache(
tags: ['fetchTokenDetails'],
}
)

export const fetchWalletBalances = unstable_cache(
async (address: string): Promise<{ balances: IUserBalance[]; totalBalance: number }> => {
const mobulaResponse = await fetchWithSentry(`https://api.mobula.io/api/1/wallet/portfolio?wallet=${address}`, {
headers: {
'Content-Type': 'application/json',
authorization: process.env.MOBULA_API_KEY!,
},
})

if (!mobulaResponse.ok) throw new Error('Failed to fetch wallet balances')

const json: { data: IMobulaPortfolioData } = await mobulaResponse.json()
const assets = json.data.assets
.filter((a: IMobulaAssetData) => !!a.price)
.filter((a: IMobulaAssetData) => !!a.token_balance)
const balances = []
for (const asset of assets) {
const symbol = asset.asset.symbol
const price = isStableCoin(symbol) || estimateIfIsStableCoinFromPrice(asset.price) ? 1 : asset.price
/*
Mobula returns balances per asset, IE: USDC on arbitrum, mainnet
and optimism are all part of the same "asset", here we need to
divide it
*/
for (const chain of asset.asset.blockchains) {
const address = asset.cross_chain_balances[chain].address
const contractInfo = asset.contracts_balances.find((c) => areEvmAddressesEqual(c.address, address))
const crossChainBalance = asset.cross_chain_balances[chain]
balances.push({
chainId: crossChainBalance.chainId,
address,
name: asset.asset.name,
symbol,
decimals: contractInfo!.decimals,
price,
amount: crossChainBalance.balance,
currency: 'usd',
logoURI: asset.asset.logo,
value: (crossChainBalance.balance * price).toString(),
})
}
}
const totalBalance = balances.reduce(
(acc: number, balance: IUserBalance) => acc + balance.amount * balance.price,
0
)
return {
balances,
totalBalance,
}
},
['fetchWalletBalances'],
{
tags: ['fetchWalletBalances'],
revalidate: 5, // 5 seconds
}
)
37 changes: 0 additions & 37 deletions src/app/api/walletconnect/fetch-wallet-balance/route.ts

This file was deleted.

Loading
Loading