Skip to content
23 changes: 17 additions & 6 deletions src/app/(mobile-ui)/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import { useDeviceType, DeviceType } from '@/hooks/useGetDeviceType'
import useKycStatus from '@/hooks/useKycStatus'
import HomeBanners from '@/components/Home/HomeBanners'
import InvitesIcon from '@/components/Home/InvitesIcon'
import NoMoreJailModal from '@/components/Global/NoMoreJailModal'
import EarlyUserModal from '@/components/Global/EarlyUserModal'

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
Expand All @@ -66,7 +68,7 @@ export default function Home() {
const [showAddMoneyPromptModal, setShowAddMoneyPromptModal] = useState(false)
const [showBalanceWarningModal, setShowBalanceWarningModal] = useState(false)
// const [showReferralCampaignModal, setShowReferralCampaignModal] = useState(false)
const [isPostSignupActionModalVisible, setIsPostSignupActionModalVisible] = useState(false)
const [showPostSignupActionModal, setShowPostSignupActionModal] = useState(false)

const userFullName = useMemo(() => {
if (!user) return
Expand Down Expand Up @@ -127,7 +129,7 @@ export default function Home() {
!isStandalone &&
!hasSeenModalThisSession &&
!user?.hasPwaInstalled &&
!isPostSignupActionModalVisible &&
!showPostSignupActionModal &&
!redirectUrl
) {
setShowIOSPWAInstallModal(true)
Expand All @@ -136,7 +138,7 @@ export default function Home() {
setShowIOSPWAInstallModal(false)
}
}
}, [user?.hasPwaInstalled, isPostSignupActionModalVisible, deviceType])
}, [user?.hasPwaInstalled, showPostSignupActionModal, deviceType])

// effect for showing balance warning modal
useEffect(() => {
Expand All @@ -155,7 +157,7 @@ export default function Home() {
!hasSeenBalanceWarning &&
!showIOSPWAInstallModal &&
!showAddMoneyPromptModal &&
!isPostSignupActionModalVisible
!showPostSignupActionModal
) {
setShowBalanceWarningModal(true)
}
Expand Down Expand Up @@ -189,20 +191,25 @@ export default function Home() {
useEffect(() => {
if (typeof window !== 'undefined' && !isFetchingBalance) {
const hasSeenAddMoneyPromptThisSession = sessionStorage.getItem('hasSeenAddMoneyPromptThisSession')
const showNoMoreJailModal = sessionStorage.getItem('showNoMoreJailModal')

// show if:
// 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.
// 5. the early user modal is not currently active.
// 6. the no more jail 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 &&
!showBalanceWarningModal &&
!isPostSignupActionModalVisible
!showPostSignupActionModal &&
showNoMoreJailModal !== 'true' &&
!user?.showEarlyUserModal // Give Early User and No more jail modal precedence, showing two modals together isn't ideal and it messes up their functionality
) {
setShowAddMoneyPromptModal(true)
sessionStorage.setItem('hasSeenAddMoneyPromptThisSession', 'true')
Expand Down Expand Up @@ -268,6 +275,10 @@ export default function Home() {
{/* Add Money Prompt Modal */}
<AddMoneyPromptModal visible={showAddMoneyPromptModal} onClose={() => setShowAddMoneyPromptModal(false)} />

<NoMoreJailModal />

<EarlyUserModal />

{/* Balance Warning Modal */}
<BalanceWarningModal
visible={showBalanceWarningModal}
Expand All @@ -287,7 +298,7 @@ export default function Home() {
{/* <FloatingReferralButton onClick={() => setShowReferralCampaignModal(true)} /> */}

{/* Post Signup Action Modal */}
<PostSignupActionManager onActionModalVisibilityChange={setIsPostSignupActionModalVisible} />
<PostSignupActionManager onActionModalVisibilityChange={setShowPostSignupActionModal} />
</PageContainer>
)
}
Expand Down
52 changes: 52 additions & 0 deletions src/components/Global/EarlyUserModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use client'
import { useEffect, useState } from 'react'
import ActionModal from '../ActionModal'
import ShareButton from '../ShareButton'
import { generateInviteCodeLink, generateInvitesShareText } from '@/utils'
import { useAuth } from '@/context/authContext'
import { updateUserById } from '@/app/actions/users'

const EarlyUserModal = () => {
const { user } = useAuth()
const inviteLink = generateInviteCodeLink(user?.user.username ?? '').inviteLink
const [showModal, setShowModal] = useState(false)

useEffect(() => {
if (user && user.showEarlyUserModal) {
setShowModal(true)
}
}, [user])

const handleCloseModal = () => {
setShowModal(false)
updateUserById({ userId: user?.user.userId, hasSeenEarlyUserModal: true })
}

return (
<ActionModal
icon="lock"
title="You’re part of the first crew"
visible={showModal}
onClose={handleCloseModal}
content={
<>
<p className="text-sm text-grey-1">
Peanut is now <b>invite-only.</b>
<br />
As an <b>early user</b>, you keep full access and you get the power to invite friends.
<br />
Each invite earns you <b>points</b> and perks.
</p>
<ShareButton
generateText={() => Promise.resolve(generateInvitesShareText(inviteLink))}
title="Share your invite link"
>
Share Invite link
</ShareButton>
</>
}
/>
)
}

export default EarlyUserModal
3 changes: 3 additions & 0 deletions src/components/Global/Icons/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { QuestionMarkIcon } from './question-mark'
import { ShieldIcon } from './shield'
import { TrophyIcon } from './trophy'
import { InviteHeartIcon } from './invite-heart'
import { LockIcon } from './lock'

// available icon names
export type IconName =
Expand Down Expand Up @@ -126,6 +127,7 @@ export type IconName =
| 'question-mark'
| 'trophy'
| 'invite-heart'
| 'lock'

export interface IconProps extends SVGProps<SVGSVGElement> {
name: IconName
Expand Down Expand Up @@ -196,6 +198,7 @@ const iconComponents: Record<IconName, ComponentType<SVGProps<SVGSVGElement>>> =
shield: ShieldIcon,
trophy: TrophyIcon,
'invite-heart': InviteHeartIcon,
lock: LockIcon,
}

export const Icon: FC<IconProps> = ({ name, size = 24, width, height, ...props }) => {
Expand Down
19 changes: 19 additions & 0 deletions src/components/Global/Icons/lock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { FC, SVGProps } from 'react'

export const LockIcon: FC<SVGProps<SVGSVGElement>> = (props) => {
return (
<svg
width="16"
height="20"
viewBox="0 0 16 20"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M13.4286 6.83333H12.5239V5.02381C12.5239 2.52667 10.4972 0.5 8.00006 0.5C5.50292 0.5 3.47625 2.52667 3.47625 5.02381V6.83333H2.57149C1.57625 6.83333 0.761963 7.64762 0.761963 8.64286V17.6905C0.761963 18.6857 1.57625 19.5 2.57149 19.5H13.4286C14.4239 19.5 15.2382 18.6857 15.2382 17.6905V8.64286C15.2382 7.64762 14.4239 6.83333 13.4286 6.83333ZM5.28577 5.02381C5.28577 3.5219 6.49815 2.30952 8.00006 2.30952C9.50196 2.30952 10.7143 3.5219 10.7143 5.02381V6.83333H5.28577V5.02381ZM13.4286 17.6905H2.57149V8.64286H13.4286V17.6905ZM8.00006 14.9762C8.9953 14.9762 9.80958 14.1619 9.80958 13.1667C9.80958 12.1714 8.9953 11.3571 8.00006 11.3571C7.00482 11.3571 6.19053 12.1714 6.19053 13.1667C6.19053 14.1619 7.00482 14.9762 8.00006 14.9762Z"
fill="currentColor"
/>
</svg>
)
}
71 changes: 71 additions & 0 deletions src/components/Global/NoMoreJailModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
'use client'
import React, { FC, 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 chillPeanutAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_01.gif'

const NoMoreJailModal = () => {
const [isOpen, setisOpen] = useState(false)

const onClose = () => {
setisOpen(false)
sessionStorage.removeItem('showNoMoreJailModal')
}

useEffect(() => {
const showNoMoreJailModal = sessionStorage.getItem('showNoMoreJailModal')
if (showNoMoreJailModal === 'true') {
setisOpen(true)
}
}, [])

return (
<Modal
hideOverlay
visible={isOpen}
onClose={onClose}
className="items-center rounded-none md:mx-auto md:max-w-md"
classWrap="sm:m-auto sm:self-center self-center m-4 bg-background rounded-none border-0"
>
{/* Main content container */}
<div className="relative z-10 w-full rounded-md bg-white px-6 py-6">
<div className="space-y-4">
<div className="space-y-3 text-center">
<div className="w-full space-y-2">
<h3 className={'text-xl font-extrabold text-black dark:text-white'}>
No more Peanut jail!
</h3>

<div className="text-sm text-grey-1 dark:text-white">
<p>
You’re now part of Peanut!
<br />
Explore, pay, and invite your friends.
</p>
</div>
</div>
</div>

<Button className="w-full" shadowSize="4" variant="purple" onClick={onClose}>
<div>Start using</div>
<div className="flex items-center gap-1">
<Image src={PEANUTMAN_LOGO} alt="Peanut Logo" className="size-5" />
<Image src={PEANUT_LOGO_BLACK} alt="Peanut Logo" />
</div>
</Button>
</div>
</div>

{/* Peanutman animation */}
<div className="absolute left-0 top-7 flex w-full justify-center" style={{ transform: 'translateY(-80%)' }}>
<div className="relative h-42 w-[90%] md:h-52">
<Image src={chillPeanutAnim.src} alt="Peanut Man" className="object-contain" fill />
</div>
</div>
</Modal>
)
}

export default NoMoreJailModal
29 changes: 1 addition & 28 deletions src/components/Home/InvitesIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,8 @@
import STAR_STRAIGHT_ICON from '@/assets/icons/starStraight.svg'
import Image from 'next/image'
import { motion } from 'framer-motion'

const InvitesIcon = () => {
return (
<motion.div
animate={{
rotate: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 15, -10, 10, -5, 0],
y: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -1, 0, -1, 0],
}}
transition={{
duration: 10,
repeat: Infinity,
ease: 'easeInOut',
}}
whileHover={{
scale: 1.3,
rotate: 20,
transition: { duration: 0.3, ease: 'easeOut' },
}}
whileTap={{
scale: 0.9,
transition: { duration: 0.1 },
}}
style={{
transition: 'transform 0.3s ease-out',
}}
>
<Image src={STAR_STRAIGHT_ICON} alt="star" width={20} height={20} />
</motion.div>
)
return <Image className="animate-pulsate-slow" src={STAR_STRAIGHT_ICON} alt="star" width={20} height={20} />
}

export default InvitesIcon
1 change: 1 addition & 0 deletions src/components/Invites/JoinWaitlistPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const JoinWaitlistPage = () => {
try {
const res = await invitesApi.acceptInvite(inviteCode, inviteType)
if (res.success) {
sessionStorage.setItem('showNoMoreJailModal', 'true')
fetchUser()
} else {
setError('Something went wrong. Please try again or contact support.')
Expand Down
11 changes: 5 additions & 6 deletions src/components/Profile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import NavHeader from '../Global/NavHeader'
import ProfileHeader from './components/ProfileHeader'
import ProfileMenuItem from './components/ProfileMenuItem'
import { useRouter } from 'next/navigation'
import { checkIfInternalNavigation, generateInvitesShareText } from '@/utils'
import { checkIfInternalNavigation, generateInviteCodeLink, generateInvitesShareText } from '@/utils'
import ActionModal from '../Global/ActionModal'
import { useState } from 'react'
import useKycStatus from '@/hooks/useKycStatus'
Expand All @@ -31,8 +31,7 @@ export const Profile = () => {
const fullName = user?.user.fullName || user?.user?.username || 'Anonymous User'
const username = user?.user.username || 'anonymous'

const inviteCode = `${user?.user.username?.toUpperCase()}INVITESYOU`
const inviteLink = `${process.env.NEXT_PUBLIC_BASE_URL}/invite?code=${inviteCode}`
const inviteData = generateInviteCodeLink(user?.user.username ?? '')

return (
<div className="h-full w-full bg-background">
Expand Down Expand Up @@ -162,13 +161,13 @@ export const Profile = () => {
<>
<div className="flex w-full items-center justify-between gap-3">
<Card className="flex items-center justify-between py-2">
<p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold ">{`${inviteCode}`}</p>
<p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold ">{`${inviteData.inviteCode}`}</p>

<CopyToClipboard textToCopy={`${inviteCode}`} />
<CopyToClipboard textToCopy={`${inviteData.inviteCode}`} />
</Card>
</div>
<ShareButton
generateText={() => Promise.resolve(generateInvitesShareText(inviteLink))}
generateText={() => Promise.resolve(generateInvitesShareText(inviteData.inviteLink))}
title="Share your invite link"
>
Share Invite link
Expand Down
16 changes: 12 additions & 4 deletions src/components/Setup/Views/JoinWaitlist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Button } from '@/components/0_Bruddle'
import { useToast } from '@/components/0_Bruddle/Toast'
import ValidatedInput from '@/components/Global/ValidatedInput'
import { useZeroDev } from '@/hooks/useZeroDev'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { twMerge } from 'tailwind-merge'
import * as Sentry from '@sentry/nextjs'
import { useSetupFlow } from '@/hooks/useSetupFlow'
Expand All @@ -14,6 +14,7 @@ import { invitesApi } from '@/services/invites'
import { useRouter, useSearchParams } from 'next/navigation'
import { getFromLocalStorage, sanitizeRedirectURL } from '@/utils'
import ErrorAlert from '@/components/Global/ErrorAlert'
import { useAuth } from '@/context/authContext'

const JoinWaitlist = () => {
const [inviteCode, setInviteCode] = useState('')
Expand All @@ -28,6 +29,7 @@ const JoinWaitlist = () => {
const dispatch = useAppDispatch()
const router = useRouter()
const searchParams = useSearchParams()
const { user } = useAuth()

const validateInviteCode = async (inviteCode: string): Promise<boolean> => {
try {
Expand Down Expand Up @@ -61,6 +63,14 @@ const JoinWaitlist = () => {
const onLoginClick = async () => {
try {
await handleLogin()
} catch (e) {
handleError(e)
}
}

// Wait for user to be fetched, then redirect
useEffect(() => {
if (user) {
const localStorageRedirect = getFromLocalStorage('redirect')
const redirect_uri = searchParams.get('redirect_uri')
if (redirect_uri) {
Expand All @@ -77,10 +87,8 @@ const JoinWaitlist = () => {
} else {
router.push('/home')
}
} catch (e) {
handleError(e)
}
}
}, [user, router, searchParams])

return (
<div className="flex flex-col gap-4">
Expand Down
Loading
Loading