Skip to content
Merged
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
46 changes: 21 additions & 25 deletions src/app/(mobile-ui)/history/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,49 +9,42 @@ import NavHeader from '@/components/Global/NavHeader'
import PeanutLoading from '@/components/Global/PeanutLoading'
import { TransactionBadge } from '@/components/Global/TransactionBadge'
import { useWallet } from '@/hooks/wallet/useWallet'
import { IDashboardItem } from '@/interfaces'
import {
formatAmount,
formatDate,
formatPaymentStatus,
getChainLogo,
getHeaderTitle,
getHistoryTransactionStatus,
getTokenLogo,
isStableCoin,
printableAddress,
formatPaymentStatus,
} from '@/utils'
import * as Sentry from '@sentry/nextjs'
import { useInfiniteQuery } from '@tanstack/react-query'
import { keepPreviousData, useInfiniteQuery, useQuery } from '@tanstack/react-query'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useEffect, useRef, useState } from 'react'
import { isAddress } from 'viem'
import { useEffect, useRef } from 'react'

const ITEMS_PER_PAGE = 10

const HistoryPage = () => {
const pathname = usePathname()
const { address } = useWallet()
const { composeLinkDataArray, fetchLinkDetailsAsync } = useDashboard()
const [dashboardData, setDashboardData] = useState<IDashboardItem[]>([])
const [isLoadingDashboard, setIsLoadingDashboard] = useState(true)
const loaderRef = useRef<HTMLDivElement>(null)

useEffect(() => {
let isStale = false
setIsLoadingDashboard(true)
composeLinkDataArray(address ?? '').then((data) => {
if (isStale) return
setDashboardData(data)
setIsLoadingDashboard(false)
})
return () => {
isStale = true
}
}, [address])
const { data: dashboardData, isLoading: isLoadingDashboard } = useQuery({
queryKey: ['dashboardData', address],
queryFn: () => composeLinkDataArray(address ?? ''),
enabled: !!address,
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes
placeholderData: keepPreviousData,
})

const fetchHistoryPage = async ({ pageParam = 0 }) => {
if (!dashboardData) return { items: [], nextPage: undefined }

const start = pageParam * ITEMS_PER_PAGE
const end = start + ITEMS_PER_PAGE
const pageData = dashboardData.slice(start, end)
Expand Down Expand Up @@ -90,16 +83,19 @@ const HistoryPage = () => {

return {
items: formattedData,
nextPage: end < dashboardData.length ? pageParam + 1 : undefined,
nextPage: end < (dashboardData?.length || 0) ? pageParam + 1 : undefined,
}
}

const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status, isLoading, error } = useInfiniteQuery({
queryKey: ['history', address],
queryKey: ['history', address, dashboardData],
queryFn: fetchHistoryPage,
getNextPageParam: (lastPage) => lastPage.nextPage,
enabled: dashboardData.length > 0,
enabled: !!dashboardData && dashboardData.length > 0,
initialPageParam: 0,
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes
placeholderData: keepPreviousData,
})

useEffect(() => {
Expand All @@ -122,7 +118,7 @@ const HistoryPage = () => {
return () => observer.disconnect()
}, [hasNextPage, isFetchingNextPage, fetchNextPage])

if (isLoadingDashboard || isLoading) {
if ((isLoadingDashboard || isLoading) && (!dashboardData || !data?.pages?.length)) {
return <PeanutLoading />
}

Expand All @@ -132,7 +128,7 @@ const HistoryPage = () => {
return <div className="w-full py-4 text-center">Error loading history: {error?.message}</div>
}

if (dashboardData.length === 0) {
if (!dashboardData || dashboardData.length === 0) {
return (
<div className="flex h-[80dvh] items-center justify-center">
<NoDataEmptyState
Expand Down
27 changes: 24 additions & 3 deletions src/app/(mobile-ui)/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ export default function Home() {
return prefs?.balanceHidden ?? false
})

const { username } = useAuth()
// state to track if content is ready to show
const [contentReady, setContentReady] = useState(false)

const { username, isFetchingUser } = useAuth()

const { selectedWallet, wallets, isWalletConnected, isFetchingWallets } = useWallet()
const { focusedWallet: focusedWalletId } = useWalletStore()
Expand Down Expand Up @@ -156,8 +159,25 @@ export default function Home() {
return wallets.length <= focusedIndex
}, [focusedIndex, wallets?.length])

if (isFetchingWallets) {
return <PeanutLoading />
const isLoading = (isFetchingWallets && !wallets.length) || (isFetchingUser && !username)

// use effect to delay showing content
useEffect(() => {
if (!isLoading) {
// small delay to ensure everything is loaded
const timer = setTimeout(() => {
setContentReady(true)
}, 100)

return () => clearTimeout(timer)
} else {
setContentReady(false)
}
}, [isLoading])

// show loading if we're loading or content isn't ready yet
if (isLoading || !contentReady) {
return <PeanutLoading coverFullScreen />
}

return (
Expand All @@ -173,6 +193,7 @@ export default function Home() {
</div>
<ProfileSection />
</div>

<div>
<div
className={classNames('relative h-[200px] p-4 sm:overflow-visible', {
Expand Down
24 changes: 18 additions & 6 deletions src/app/(mobile-ui)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import { GenericBanner } from '@/components/Global/Banner/GenericBanner'
import GuestLoginModal from '@/components/Global/GuestLoginModal'
import PeanutLoading from '@/components/Global/PeanutLoading'
import TopNavbar from '@/components/Global/TopNavbar'
import WalletNavigation from '@/components/Global/WalletNavigation'
import HomeWaitlist from '@/components/Home/HomeWaitlist'
import { ThemeProvider } from '@/config'
import { peanutWalletIsInPreview } from '@/constants'
import { useAuth } from '@/context/authContext'
import { hasValidJwtToken } from '@/utils/auth'
import classNames from 'classnames'
import { usePathname } from 'next/navigation'
import { useEffect, useMemo, useState } from 'react'
Expand All @@ -18,12 +20,9 @@ const publicPathRegex = /^\/(request\/pay|claim)/

const Layout = ({ children }: { children: React.ReactNode }) => {
const pathName = usePathname()
const { isFetchingUser, user } = useAuth()
const [isReady, setIsReady] = useState(false)
const { user } = useAuth()

useEffect(() => {
setIsReady(true)
}, [])
const [hasToken, setHasToken] = useState(false)

const isHome = pathName === '/home'
const isHistory = pathName === '/history'
Expand All @@ -36,7 +35,20 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
return isPublicPath || (user?.user.hasPwAccess ?? false) || !peanutWalletIsInPreview
}, [user, pathName])

if (!isReady) return null
useEffect(() => {
// check for JWT token
setHasToken(hasValidJwtToken())

setIsReady(true)
}, [])

if (!isReady || (isFetchingUser && !user && !hasToken))
return (
<div className="flex h-[100dvh] w-full flex-col items-center justify-center">
<PeanutLoading />
</div>
)

return (
<div className="flex h-[100dvh] w-full bg-background">
{/* Wrapper div for desktop layout */}
Expand Down
18 changes: 15 additions & 3 deletions src/app/(setup)/setup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
'use client'

import PeanutLoading from '@/components/Global/PeanutLoading'
import { SetupWrapper } from '@/components/Setup/components/SetupWrapper'
import { useSetupFlow } from '@/hooks/useSetupFlow'
import { useSetupStore, useAppDispatch } from '@/redux/hooks'
import { useEffect, useState } from 'react'
import { BeforeInstallPromptEvent } from '@/components/Setup/Setup.types'
import { useSetupFlow } from '@/hooks/useSetupFlow'
import { useAppDispatch, useSetupStore } from '@/redux/hooks'
import { setupActions } from '@/redux/slices/setup-slice'
import { useEffect, useState } from 'react'

export default function SetupPage() {
const { steps } = useSetupStore()
Expand All @@ -17,10 +18,12 @@ export default function SetupPage() {
const [deviceType, setDeviceType] = useState<'ios' | 'android' | 'desktop'>('desktop')
const dispatch = useAppDispatch()
const [unsupportedBrowser, setUnsupportedBrowser] = useState(false)
const [isLoading, setIsLoading] = useState(true)

useEffect(() => {
// Check if the browser supports passkeys
const checkPasskeySupport = async () => {
setIsLoading(true)
try {
const hasPasskeySupport = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
setUnsupportedBrowser(!hasPasskeySupport)
Expand All @@ -39,6 +42,8 @@ export default function SetupPage() {
if (unsupportedBrowserIndex !== -1) {
dispatch(setupActions.setStep(unsupportedBrowserIndex + 1))
}
} finally {
setIsLoading(false)
}
}

Expand Down Expand Up @@ -84,6 +89,13 @@ export default function SetupPage() {
}
}, [step, currentStepIndex, steps])

if (isLoading)
return (
<div className="flex h-[100dvh] w-full flex-col items-center justify-center">
<PeanutLoading />
</div>
)

// todo: add loading state
if (!step) return null

Expand Down
12 changes: 9 additions & 3 deletions src/components/Global/PeanutLoading/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { PEANUTMAN_LOGO } from '@/assets'
import { twMerge } from 'tailwind-merge'

export default function PeanutLoading() {
export default function PeanutLoading({ coverFullScreen = false }: { coverFullScreen?: boolean }) {
return (
<div className="relative flex w-full items-center justify-center self-center">
<div className="animate-spin">
<div
className={twMerge(
'relative flex w-full items-center justify-center self-center',
coverFullScreen && 'fixed top-0 z-50 flex h-screen w-full items-center justify-center bg-background'
)}
>
<div className={twMerge('animate-spin')}>
<img src={PEANUTMAN_LOGO.src} alt="logo" className="h-6 sm:h-10" />
<span className="sr-only">Loading...</span>
</div>
Expand Down
7 changes: 2 additions & 5 deletions src/components/Home/HomeWaitlist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useEffect } from 'react'
import { Card } from '../0_Bruddle'
import RollingNumber from '../0_Bruddle/RollingNumber'
import Title from '../0_Bruddle/Title'
import PeanutLoading from '../Global/PeanutLoading'

const HomeWaitlist = () => {
const { push } = useRouter()
Expand All @@ -17,11 +18,7 @@ const HomeWaitlist = () => {
}, [username, isFetchingUser, push])

if (isFetchingUser) {
return (
<div className="flex h-full w-full flex-col items-center justify-center">
<img src={chillPeanutAnim.src} alt="peanut-club" className="w-[300px] object-cover" />
</div>
)
return <PeanutLoading coverFullScreen />
}

return (
Expand Down
6 changes: 3 additions & 3 deletions src/components/Setup/components/SetupWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import starImage from '@/assets/icons/star.png'
import { Button } from '@/components/0_Bruddle'
import CloudsBackground from '@/components/0_Bruddle/CloudsBackground'
import Icon from '@/components/Global/Icon'
import { BeforeInstallPromptEvent, LayoutType, ScreenId } from '@/components/Setup/Setup.types'
import InstallPWA from '@/components/Setup/Views/InstallPWA'
import classNames from 'classnames'
import Image from 'next/image'
import { ReactNode, memo, Children, cloneElement, type ReactElement } from 'react'
import { Children, ReactNode, cloneElement, memo, type ReactElement } from 'react'
import { twMerge } from 'tailwind-merge'
import { LayoutType, ScreenId, BeforeInstallPromptEvent } from '@/components/Setup/Setup.types'
import InstallPWA from '@/components/Setup/Views/InstallPWA'

/**
* props interface for the SetupWrapper component
Expand Down
4 changes: 3 additions & 1 deletion src/hooks/query/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useAppDispatch, useUserStore } from '@/redux/hooks'
import { userActions } from '@/redux/slices/user-slice'
import { fetchWithSentry } from '@/utils'
import { hitUserMetric } from '@/utils/metrics.utils'
import { useQuery } from '@tanstack/react-query'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { usePWAStatus } from '../usePWAStatus'

export const useUserQuery = (dependsOn?: boolean) => {
Expand Down Expand Up @@ -45,5 +45,7 @@ export const useUserQuery = (dependsOn?: boolean) => {
refetchOnMount: false,
// add initial data from Redux if available
initialData: authUser || undefined,
// keep previous data
placeholderData: keepPreviousData,
})
}
13 changes: 8 additions & 5 deletions src/hooks/wallet/useWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { useAppDispatch, useWalletStore } from '@/redux/hooks'
import { walletActions } from '@/redux/slices/wallet-slice'
import { areEvmAddressesEqual, backgroundColorFromAddress, fetchWalletBalances, formatAmount } from '@/utils'
import * as Sentry from '@sentry/nextjs'
import { useQuery } from '@tanstack/react-query'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { useCallback, useEffect, useMemo } from 'react'
import { erc20Abi, formatUnits, getAddress, parseUnits } from 'viem'
import { useAccount } from 'wagmi'
Expand Down Expand Up @@ -234,15 +234,18 @@ export const useWallet = () => {
queryKey: ['wallets', user, user?.accounts, wagmiAddress],
queryFn: mergeAndProcessWallets,
enabled: !!wagmiAddress || !!user,
staleTime: 30 * 1000, // 30 seconds
gcTime: 1 * 60 * 1000, // 1 minute
staleTime: 60 * 1000, // 60 seconds
gcTime: 5 * 60 * 1000, // 5 minutes
placeholderData: keepPreviousData,
})

useEffect(() => {
if (isWalletsQueryLoading) {
if (isWalletsQueryLoading && !wallets.length) {
dispatch(walletActions.setIsFetchingWallets(true))
} else if (!isWalletsQueryLoading) {
dispatch(walletActions.setIsFetchingWallets(false))
}
}, [isWalletsQueryLoading])
}, [isWalletsQueryLoading, wallets.length])

const selectedWallet = useMemo(() => {
if (!selectedWalletId || !wallets.length) return undefined
Expand Down
36 changes: 36 additions & 0 deletions src/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* checks if there's a valid JWT token in cookies and if it hasn't expired
* @returns boolean indicating if a valid JWT token exists
*/
export const hasValidJwtToken = (): boolean => {
if (typeof document === 'undefined') return false

const cookies = document.cookie.split(';')
const jwtCookie = cookies.find((cookie) => cookie.trim().startsWith('jwt-token='))

if (!jwtCookie) return false

const token = jwtCookie.split('=')[1]
if (!token) return false

// validation - check if token has three parts separated by dots
const parts = token.split('.')
if (parts.length !== 3) return false

try {
// decode payload (middle part) to check expiration
const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')))

// check if token has an expiration claim
if (!payload.exp) return true // if no expiration, consider it valid

// check if token has expired
const expirationTime = payload.exp * 1000 // convert to milliseconds
const currentTime = Date.now()

return currentTime < expirationTime
} catch (error) {
console.error('Error parsing JWT token:', error)
return false
}
}
Loading