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
56 changes: 23 additions & 33 deletions src/app/(mobile-ui)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,17 @@ import { useAuth } from '@/context/authContext'
import { hasValidJwtToken } from '@/utils/auth'
import classNames from 'classnames'
import { usePathname } from 'next/navigation'
import PullToRefresh from 'pulltorefreshjs'
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { twMerge } from 'tailwind-merge'
import '../../styles/globals.css'
import SupportDrawer from '@/components/Global/SupportDrawer'
import JoinWaitlistPage from '@/components/Invites/JoinWaitlistPage'
import { useRouter } from 'next/navigation'
import { Banner } from '@/components/Global/Banner'
import { DeviceType, useDeviceType } from '@/hooks/useGetDeviceType'
import { useSetupStore } from '@/redux/hooks'
import ForceIOSPWAInstall from '@/components/ForceIOSPWAInstall'
import { PUBLIC_ROUTES_REGEX } from '@/constants/routes'
import { usePullToRefresh } from '@/hooks/usePullToRefresh'

const Layout = ({ children }: { children: React.ReactNode }) => {
const pathName = usePathname()
Expand All @@ -37,48 +36,39 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
const isSupport = pathName === '/support'
const alignStart = isHome || isHistory || isSupport
const router = useRouter()
const { deviceType: detectedDeviceType } = useDeviceType()
const { showIosPwaInstallScreen } = useSetupStore()

// cache the scrollable content element to avoid DOM queries on every scroll event
const scrollableContentRef = useRef<Element | null>(null)

useEffect(() => {
// check for JWT token
setHasToken(hasValidJwtToken())

setIsReady(true)
}, [])

// Pull-to-refresh is only enabled on iOS devices since Android has native pull-to-refresh
// docs here: https://github.com/BoxFactura/pulltorefresh.js
useEffect(() => {
if (typeof window === 'undefined') return

// Only initialize pull-to-refresh on iOS devices
if (detectedDeviceType !== DeviceType.IOS) return

PullToRefresh.init({
mainElement: 'body',
onRefresh: () => {
window.location.reload()
},
instructionsPullToRefresh: 'Pull down to refresh',
instructionsReleaseToRefresh: 'Release to refresh',
instructionsRefreshing: 'Refreshing...',
shouldPullToRefresh: () => {
const el = document.querySelector('body')
if (!el) return false

return el.scrollTop === 0 && window.scrollY === 0
},
distThreshold: 70,
distMax: 120,
distReload: 80,
})

return () => {
PullToRefresh.destroyAll()
// memoizing shouldPullToRefresh callback to prevent re-initialization on every render
// lazy-load element ref to ensure DOM is ready
const shouldPullToRefresh = useCallback(() => {
// lazy-load the element reference if not cached yet
if (!scrollableContentRef.current) {
scrollableContentRef.current = document.querySelector('#scrollable-content')
}

const scrollableContent = scrollableContentRef.current
if (!scrollableContent) {
// fallback to window scroll check if element not found
return window.scrollY === 0
}

// only allow pull-to-refresh when at the very top
return scrollableContent.scrollTop === 0
}, [])

// enable pull-to-refresh for both ios and android
usePullToRefresh({ shouldPullToRefresh })

useEffect(() => {
if (!isPublicPath && !isFetchingUser && !user) {
router.push('/setup')
Expand Down
3 changes: 3 additions & 0 deletions src/app/(setup)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import '../../styles/globals.css'
import PeanutLoading from '@/components/Global/PeanutLoading'
import { Banner } from '@/components/Global/Banner'
import { DeviceType, useDeviceType } from '@/hooks/useGetDeviceType'
import { usePullToRefresh } from '@/hooks/usePullToRefresh'

function SetupLayoutContent({ children }: { children?: React.ReactNode }) {
const dispatch = useAppDispatch()
Expand All @@ -33,6 +34,8 @@ function SetupLayoutContent({ children }: { children?: React.ReactNode }) {
}
}, [isPWA, deviceType])

usePullToRefresh()

return (
<>
<Banner />
Expand Down
1 change: 1 addition & 0 deletions src/app/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default function manifest(): MetadataRoute.Manifest {
description: 'Butter smooth global money',
start_url: '/home',
display: 'standalone',
display_override: ['standalone'],
background_color: '#ffffff',
theme_color: '#000000',
icons: [
Expand Down
58 changes: 31 additions & 27 deletions src/components/Global/WalletNavigation/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client'
import { PEANUT_LOGO } from '@/assets'
import DirectSendQr from '@/components/Global/DirectSendQR'
import { Icon, type IconName, Icon as NavIcon } from '@/components/Global/Icons/Icon'
Expand All @@ -7,7 +8,7 @@ import { useUserStore } from '@/redux/hooks'
import classNames from 'classnames'
import Image from 'next/image'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { usePathname, useRouter } from 'next/navigation'
import { useHaptic } from 'use-haptic'

type NavPathProps = {
Expand All @@ -33,32 +34,35 @@ type NavSectionProps = {
pathName: string
}

const NavSection: React.FC<NavSectionProps> = ({ paths, pathName }) => (
<>
{paths.map(({ name, href, icon, size }, index) => (
<div key={`${name}-${index}`}>
<Link
href={href}
className={classNames(
'flex items-center gap-3 text-white hover:cursor-pointer hover:text-white/80',
{
'text-primary-1': pathName === href,
}
)}
onClick={() => {
if (pathName === href) {
window.location.reload()
}
}}
>
<Icon name={icon} className="block text-white" size={size} />
<span className="block w-fit pt-0.5 text-center text-base font-semibold">{name}</span>
</Link>
{index === 4 && <div className="w-full border-b border-grey-1 pt-5" />}
</div>
))}
</>
)
const NavSection: React.FC<NavSectionProps> = ({ paths, pathName }) => {
const router = useRouter()
return (
<>
{paths.map(({ name, href, icon, size }, index) => (
<div key={`${name}-${index}`}>
<Link
href={href}
className={classNames(
'flex items-center gap-3 text-white hover:cursor-pointer hover:text-white/80',
{
'text-primary-1': pathName === href,
}
)}
onClick={() => {
if (pathName === href) {
router.refresh()
}
}}
>
<Icon name={icon} className="block text-white" size={size} />
<span className="block w-fit pt-0.5 text-center text-base font-semibold">{name}</span>
</Link>
{index === 4 && <div className="w-full border-b border-grey-1 pt-5" />}
</div>
))}
</>
)
}

type MobileNavProps = {
pathName: string
Expand Down
65 changes: 65 additions & 0 deletions src/hooks/usePullToRefresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useRouter } from 'next/navigation'
import { useEffect, useRef } from 'react'
import PullToRefresh from 'pulltorefreshjs'

// pull-to-refresh configuration constants
const DIST_THRESHOLD = 70 // minimum pull distance to trigger refresh
const DIST_MAX = 120 // maximum pull distance (visual limit)
const DIST_RELOAD = 80 // distance at which refresh is triggered when released

interface UsePullToRefreshOptions {
// custom function to determine if pull-to-refresh should be enabled
// defaults to checking if window is at the top
shouldPullToRefresh?: () => boolean
// whether to enable pull-to-refresh (defaults to true)
enabled?: boolean
}

/**
* hook to enable pull-to-refresh functionality on mobile devices
* native pull-to-refresh is disabled via css (overscroll-behavior-y: none in globals.css)
* this hook uses pulltorefreshjs library for consistent behavior across ios and android
*/
export const usePullToRefresh = (options: UsePullToRefreshOptions = {}) => {
const router = useRouter()
const { shouldPullToRefresh, enabled = true } = options

// store callback in ref to avoid re-initialization when function reference changes
const shouldPullToRefreshRef = useRef(shouldPullToRefresh)

// update ref when callback changes
useEffect(() => {
shouldPullToRefreshRef.current = shouldPullToRefresh
}, [shouldPullToRefresh])

useEffect(() => {
if (typeof window === 'undefined' || !enabled) return

// default behavior: allow pull-to-refresh when window is at the top
const defaultShouldPullToRefresh = () => window.scrollY === 0

PullToRefresh.init({
mainElement: 'body',
onRefresh: () => {
// router.refresh() returns void, wrap in promise for pulltorefreshjs
router.refresh()
return Promise.resolve()
},
instructionsPullToRefresh: 'Pull down to refresh',
instructionsReleaseToRefresh: 'Release to refresh',
instructionsRefreshing: 'Refreshing...',
shouldPullToRefresh: () => {
// use latest callback from ref
const callback = shouldPullToRefreshRef.current
return callback ? callback() : defaultShouldPullToRefresh()
},
distThreshold: DIST_THRESHOLD,
distMax: DIST_MAX,
distReload: DIST_RELOAD,
})

return () => {
PullToRefresh.destroyAll()
}
}, [router, enabled])
}
14 changes: 14 additions & 0 deletions src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
color-scheme: light;
}

html,
body {
/* disable native pull-to-refresh on mobile devices - we use custom implementation */
overscroll-behavior-y: none;
}

body {
/* disable text selection */
@apply select-none;
Expand Down Expand Up @@ -281,13 +287,15 @@ Firefox input[type='number'] {
100% {
transform: translateX(0) rotate(0deg);
}

10%,
30%,
50%,
70%,
90% {
transform: translateX(-4px) rotate(-0.5deg);
}

20%,
40%,
60%,
Expand All @@ -301,13 +309,15 @@ Firefox input[type='number'] {
100% {
transform: translateX(0) rotate(0deg);
}

10%,
30%,
50%,
70%,
90% {
transform: translateX(-8px) rotate(-1deg);
}

20%,
40%,
60%,
Expand All @@ -321,13 +331,15 @@ Firefox input[type='number'] {
100% {
transform: translate(0, 0) rotate(0deg);
}

10%,
30%,
50%,
70%,
90% {
transform: translate(-12px, -2px) rotate(-1.5deg);
}

20%,
40%,
60%,
Expand All @@ -341,13 +353,15 @@ Firefox input[type='number'] {
100% {
transform: translate(0, 0) rotate(0deg);
}

10%,
30%,
50%,
70%,
90% {
transform: translate(-16px, -3px) rotate(-2deg);
}

20%,
40%,
60%,
Expand Down
Loading