Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
35c3720
Invites UI
Zishan-7 Sep 21, 2025
306b263
add basic API integration
Zishan-7 Sep 21, 2025
6046db0
update flow and integrate valid Invite code check
Zishan-7 Sep 22, 2025
10d2fd7
feat: integrate waitlist logic
Zishan-7 Sep 24, 2025
9e7bcc8
feat: integrate /invites page
Zishan-7 Sep 24, 2025
54e298e
integrate payment link invites
Zishan-7 Sep 24, 2025
6d430da
Add copy invite logic
Zishan-7 Sep 24, 2025
9a9b0da
add collect email flow
Zishan-7 Sep 25, 2025
67cd406
fix: bugs and fixes
Zishan-7 Sep 25, 2025
9118f50
fix: improve user access checks and update image import in ActionList
Zishan-7 Sep 25, 2025
0ecd8f0
Merge remote-tracking branch 'origin/peanut-wallet-dev' into feat/inv…
Zishan-7 Sep 25, 2025
5d26f18
fix: add missing dependency
Zishan-7 Sep 25, 2025
f08a31c
fix: build error
Zishan-7 Sep 25, 2025
fcec045
fix: enhance username handling in ActionList and improve error handli…
Zishan-7 Sep 25, 2025
d76a524
refactor: simplify layout component by removing conditional rendering…
Zishan-7 Sep 28, 2025
8ef5110
add widdge animation
Zishan-7 Sep 29, 2025
9f6bd9c
fix nits and bugs
Zishan-7 Sep 29, 2025
85e098a
refactor: replace CopyToClipboardButton with CopyToClipboard component
Zishan-7 Sep 29, 2025
eaaf636
create invite interface
Zishan-7 Sep 29, 2025
e15f64b
Merge remote-tracking branch 'origin/peanut-wallet-dev' into feat/inv…
Zishan-7 Sep 29, 2025
bbb5272
Add payment invite type
Zishan-7 Sep 29, 2025
2911337
fix: minor bug and fixes
Zishan-7 Sep 29, 2025
3464c01
improve error handling
Zishan-7 Sep 29, 2025
6bb4c7b
update animation
Zishan-7 Sep 29, 2025
0a68369
update animation
Zishan-7 Sep 29, 2025
bb39196
refactor: replace star icon animation with InvitesIcon component and …
Zishan-7 Sep 30, 2025
ed23e30
fix: undefined user error
Zishan-7 Sep 30, 2025
6203071
Merge remote-tracking branch 'origin/peanut-wallet-dev' into feat/inv…
Zishan-7 Sep 30, 2025
570656f
fix: undefined user error
Zishan-7 Sep 30, 2025
e785ae0
Merge remote-tracking branch 'origin/peanut-wallet-dev' into feat/inv…
Zishan-7 Sep 30, 2025
5f8d924
fix: UI bugs and Dont lose invite not showing on daimo method
Zishan-7 Oct 1, 2025
ed5c25d
feat: add showConfirmModal prop to ActionListDaimoPayButton for invit…
Zishan-7 Oct 1, 2025
4ae9427
feat: add error handling for invite code validation in JoinWaitlist c…
Zishan-7 Oct 1, 2025
14009c8
fix: claim route not working
Zishan-7 Oct 2, 2025
08dd943
refactor: remove inviteCode dependency from useEffect in SetupLayoutC…
Zishan-7 Oct 2, 2025
f8dfbc0
Fix: browser not supported error on mobile
Zishan-7 Oct 2, 2025
a2616cc
Remove `Refer` text
Zishan-7 Oct 2, 2025
a3c38fa
design updates
Zishan-7 Oct 3, 2025
69cde04
fix: email error message
Zishan-7 Oct 3, 2025
632635e
fix: home screen flashes before logout
Zishan-7 Oct 3, 2025
0ee0f27
Merge remote-tracking branch 'origin/peanut-wallet-dev' into feat/inv…
Zishan-7 Oct 3, 2025
0e4fa50
add social preview
Zishan-7 Oct 3, 2025
4eddeaa
remove logs
Zishan-7 Oct 3, 2025
e89dca0
fix: bugs
Zishan-7 Oct 3, 2025
94f1154
update social preview description
Zishan-7 Oct 3, 2025
207142a
add peanutman logo on top of modal
Zishan-7 Oct 6, 2025
1c295fc
Merge remote-tracking branch 'origin/peanut-wallet-dev' into feat/inv…
Zishan-7 Oct 6, 2025
1228144
coderrabit suggestions
Zishan-7 Oct 6, 2025
6891653
refactor: remove unused selectedStep variable and clean up dependenci…
Zishan-7 Oct 6, 2025
8e3adb0
Merge remote-tracking branch 'origin/peanut-wallet-dev' into feat/inv…
Zishan-7 Oct 6, 2025
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
6 changes: 6 additions & 0 deletions public/arrows/top-right-arrow-2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion src/app/(mobile-ui)/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { useClaimBankFlow } from '@/context/ClaimBankFlowContext'
import { useDeviceType, DeviceType } from '@/hooks/useGetDeviceType'
import useKycStatus from '@/hooks/useKycStatus'
import HomeBanners from '@/components/Home/HomeBanners'
import InvitesIcon from '@/components/Home/InvitesIcon'

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 Down Expand Up @@ -218,7 +219,12 @@ export default function Home() {
<div className="h-full w-full space-y-6 p-5">
<div className="flex items-center justify-between gap-2">
<UserHeader username={username!} fullName={userFullName} isVerified={isUserKycApproved} />
<SearchUsers />
<div className="flex items-center gap-2">
<Link href="/points">
<InvitesIcon />
</Link>
<SearchUsers />
</div>
</div>
<div className="space-y-4">
<ActionButtonGroup>
Expand Down
12 changes: 10 additions & 2 deletions src/app/(mobile-ui)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import { twMerge } from 'tailwind-merge'
import '../../styles/globals.css'
import SupportDrawer from '@/components/Global/SupportDrawer'
import { useSupportModalContext } from '@/context/SupportModalContext'
import JoinWaitlistPage from '@/components/Invites/JoinWaitlistPage'
import { useRouter } from 'next/navigation'
import { Banner } from '@/components/Global/Banner'

// Allow access to some public paths without authentication
const publicPathRegex = /^\/(request\/pay|claim|pay\/.+$|support)/
const publicPathRegex = /^\/(request\/pay|claim|pay\/.+$|support|invite)/

const Layout = ({ children }: { children: React.ReactNode }) => {
const pathName = usePathname()
Expand Down Expand Up @@ -83,14 +84,21 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
}
}, [user, isFetchingUser])

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

// Show waitlist page if user doesn't have app access
if (!isFetchingUser && user && !user?.user.hasAppAccess) {
return <JoinWaitlistPage />
}

console.log(user, 'user')

return (
<div className="flex min-h-[100dvh] w-full bg-background">
{/* Wrapper div for desktop layout */}
Expand Down
116 changes: 96 additions & 20 deletions src/app/(mobile-ui)/points/page.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,108 @@
'use client'

import Icon from '@/components/Global/Icon'
import PageContainer from '@/components/0_Bruddle/PageContainer'
import Card, { getCardPosition } from '@/components/Global/Card'
import CopyToClipboard from '@/components/Global/CopyToClipboard'
import { Icon } from '@/components/Global/Icons/Icon'
import NavHeader from '@/components/Global/NavHeader'
import PeanutLoading from '@/components/Global/PeanutLoading'
import ShareButton from '@/components/Global/ShareButton'
import TransactionAvatarBadge from '@/components/TransactionDetails/TransactionAvatarBadge'
import { VerifiedUserLabel } from '@/components/UserHeader'
import { useAuth } from '@/context/authContext'
import peanutClub from '@/assets/peanut/peanut-club.png'
import { invitesApi } from '@/services/invites'
import { Invite } from '@/services/services.types'
import { generateInvitesShareText } from '@/utils'
import { useQuery } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'

const PointsPage = () => {
const router = useRouter()
const { user } = useAuth()
const points = user?.totalPoints ?? 0
const invites = user?.referredUsers ?? 0
const { data: invites, isLoading } = useQuery({
queryKey: ['invites', user?.user.userId],
queryFn: () => invitesApi.getInvites(),
enabled: !!user?.user.userId,
})

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

if (isLoading) {
return <PeanutLoading coverFullScreen />
}

return (
<div className="flex h-full w-full flex-col items-center justify-between">
<div className="flex flex-col gap-2">
<div>
<div className="flex flex-row items-center">
<Icon name="arrow-next" className="h-8 w-8" />
{points} Points
</div>
</div>
<div>
<div className="flex flex-row items-center">
<Icon name="arrow-next" className="h-8 w-8" />
{invites} Invites
</div>
<PageContainer className="flex flex-col">
<NavHeader title="Invites" onPrev={() => router.back()} />

<section className="mx-auto mb-auto mt-10 w-full space-y-4">
<h1 className="font-bold">Invite friends with your code</h1>
<div className="flex w-full items-center justify-between gap-3">
<Card className="flex w-full items-center justify-between py-3.5">
<p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold md:text-base">{`${inviteCode}`}</p>
<CopyToClipboard textToCopy={inviteCode} />
</Card>
</div>
</div>
<img src={peanutClub.src} className="h-[240px] w-[240px] object-contain" alt="logo" />
</div>

{invites && invites.length > 0 && (
<>
<ShareButton
generateText={() => Promise.resolve(generateInvitesShareText(inviteLink))}
title="Share your invite link"
>
Share Invite link
</ShareButton>
<h2 className="!mt-8 font-bold">People you invited</h2>
<div>
{invites?.map((invite: Invite, i: number) => {
const username = invite.invitee.username
const isVerified = invite.invitee.bridgeKycStatus === 'approved'
return (
<Card key={invite.id} position={getCardPosition(i, invites.length)}>
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<TransactionAvatarBadge
initials={username}
userName={username}
isLinkTransaction={false}
transactionType={'send'}
context="card"
size="small"
/>
</div>

<div className="min-w-0 flex-1 truncate font-roboto text-[16px] font-medium">
<VerifiedUserLabel name={username} isVerified={isVerified} />
</div>
</div>
</Card>
)
})}
</div>
</>
)}
{invites?.length === 0 && (
<Card className="flex flex-col items-center justify-center gap-4 py-4">
<div className="flex items-center justify-center rounded-full bg-primary-1 p-2">
<Icon name="trophy" />
</div>
<h2 className="font-medium">No invites yet</h2>

<p className="text-center text-sm text-grey-1">
Send your invite link to start earning more rewards
</p>
<ShareButton
generateText={() => Promise.resolve(generateInvitesShareText(inviteLink))}
title="Share your invite link"
>
Share Invite link
</ShareButton>
</Card>
)}
</section>
</PageContainer>
)
}

Expand Down
13 changes: 11 additions & 2 deletions src/app/(setup)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import { usePWAStatus } from '@/hooks/usePWAStatus'
import { useAppDispatch } from '@/redux/hooks'
import { setupActions } from '@/redux/slices/setup-slice'
import { useEffect } from 'react'
import { useEffect, Suspense } from 'react'
import { setupSteps } from '../../components/Setup/Setup.consts'
import '../../styles/globals.css'
import PeanutLoading from '@/components/Global/PeanutLoading'
import { Banner } from '@/components/Global/Banner'

const SetupLayout = ({ children }: { children?: React.ReactNode }) => {
function SetupLayoutContent({ children }: { children?: React.ReactNode }) {
const dispatch = useAppDispatch()
const isPWA = usePWAStatus()

Expand All @@ -28,4 +29,12 @@ const SetupLayout = ({ children }: { children?: React.ReactNode }) => {
)
}

const SetupLayout = ({ children }: { children?: React.ReactNode }) => {
return (
<Suspense fallback={<PeanutLoading coverFullScreen />}>
<SetupLayoutContent>{children}</SetupLayoutContent>
</Suspense>
)
}

export default SetupLayout
22 changes: 16 additions & 6 deletions src/app/(setup)/setup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ import { BeforeInstallPromptEvent, ScreenId, ISetupStep } from '@/components/Set
import { useSetupFlow } from '@/hooks/useSetupFlow'
import { useAppDispatch, useSetupStore } from '@/redux/hooks'
import { setupActions } from '@/redux/slices/setup-slice'
import { useEffect, useState } from 'react'
import ActionModal from '@/components/Global/ActionModal'
import { IconName } from '@/components/Global/Icons/Icon'
import { Suspense, useEffect, useState } from 'react'
import { setupSteps as masterSetupSteps } from '../../../components/Setup/Setup.consts'
import UnsupportedBrowserModal from '@/components/Global/UnsupportedBrowserModal'
import { isLikelyWebview, isDeviceOsSupported, getDeviceTypeForLogic } from '@/components/Setup/Setup.utils'

export default function SetupPage() {
const { steps } = useSetupStore()
function SetupPageContent() {
const { steps, inviteCode } = useSetupStore()
const { step, handleNext, handleBack } = useSetupFlow()
const [direction, setDirection] = useState(0)
const [currentStepIndex, setCurrentStepIndex] = useState(0)
Expand Down Expand Up @@ -113,7 +111,11 @@ export default function SetupPage() {
determinedSetupInitialStepId = 'pwa-install'
}

if (determinedSetupInitialStepId) {
// If invite code is present, set the step to the signup screen
if (determinedSetupInitialStepId && inviteCode) {
const signupScreenIndex = steps.findIndex((s: ISetupStep) => s.screenId === 'signup')
dispatch(setupActions.setStep(signupScreenIndex + 1))
} else if (determinedSetupInitialStepId) {
const initialStepIndex = steps.findIndex((s: ISetupStep) => s.screenId === determinedSetupInitialStepId)
if (initialStepIndex !== -1) {
dispatch(setupActions.setStep(initialStepIndex + 1))
Expand Down Expand Up @@ -208,3 +210,11 @@ export default function SetupPage() {
</SetupWrapper>
)
}

export default function SetupPage() {
return (
<Suspense fallback={<PeanutLoading coverFullScreen />}>
<SetupPageContent />
</Suspense>
)
}
3 changes: 3 additions & 0 deletions src/app/[...recipient]/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,9 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props)
flow="request"
requestLinkData={parsedPaymentData as ParsedURL}
isLoggedIn={!!user?.user.userId}
isInviteLink={
flow === 'request_pay' && parsedPaymentData?.recipient?.recipientType === 'USERNAME'
} // invite link is only available for request pay flow
/>
)}
</div>
Expand Down
43 changes: 43 additions & 0 deletions src/app/actions/invites.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use server'

import { PEANUT_API_URL } from '@/constants'
import { fetchWithSentry } from '@/utils'

const API_KEY = process.env.PEANUT_API_KEY!

export async function validateInviteCode(
inviteCode: string
): Promise<{ data?: { success: boolean; username: string }; error?: string }> {
const apiUrl = PEANUT_API_URL

if (!apiUrl || !API_KEY) {
console.error('API URL or API Key is not configured.')
return { error: 'Server configuration error.' }
}

try {
const response = await fetchWithSentry(`${apiUrl}/invites/validate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'api-key': API_KEY,
},
body: JSON.stringify({ inviteCode }),
})

if (!response.ok) {
const data = await response.json()
return { error: data.error || 'Failed to validate invite code.' }
}

const data = await response.json()

return { data: { success: true, username: data.username } }
} catch (error) {
console.error('Error calling validate invite code API:', error)
if (error instanceof Error) {
return { error: error.message }
}
return { error: 'An unexpected error occurred.' }
}
}
49 changes: 33 additions & 16 deletions src/app/api/og/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ReceiptCardOG } from '@/components/og/ReceiptCardOG'
import { printableAddress, resolveAddressToUsername } from '@/utils'
import { isAddress } from 'viem'
import { ProfileCardOG } from '@/components/og/ProfileCardOG'
import { InviteCardOG } from '@/components/og/InviteCardOG'

export const runtime = 'nodejs' //node.js instead of edge!

Expand Down Expand Up @@ -68,6 +69,38 @@ export async function GET(req: NextRequest) {
const token = searchParams.get('token') || null
const isReceipt = searchParams.get('isReceipt') || 'false'
const isPeanutUsername = searchParams.get('isPeanutUsername') || 'false'
const isInvite = searchParams.get('isInvite') || 'false'

// create an object with all arrow SVG paths
const arrowSrcs = {
topLeft: `${origin}/arrows/top-left-arrows.svg`,
topRight: `${origin}/arrows/top-right-arrow.svg`,
bottomLeft: `${origin}/arrows/bottom-left-arrow.svg`,
bottomRight: `${origin}/arrows/bottom-right-arrow.svg`,
topRight2: `${origin}/arrows/top-right-arrow-2.svg`,
}

if (isInvite === 'true') {
return new ImageResponse(
(
<InviteCardOG
username={username}
scribbleSrc={`${origin}/scribble.svg`}
iconSrc={`${origin}/icons/peanut-icon.svg`}
logoSrc={`${origin}/logos/peanut-logo.svg`}
arrowSrcs={arrowSrcs}
/>
),
{
width: 1200,
height: 630,
fonts: [
{ name: 'Montserrat Medium', data: montserratMedium, style: 'normal' },
{ name: 'Montserrat SemiBold', data: montserratSemibold, style: 'normal' },
],
}
)
}

if (type === 'generic') {
return new ImageResponse(<div style={{}}>Peanut Protocol</div>, {
Expand Down Expand Up @@ -111,14 +144,6 @@ export async function GET(req: NextRequest) {
)
}
if (isReceipt === 'true') {
// create an object with all arrow SVG paths for receipts
const arrowSrcs = {
topLeft: `${origin}/arrows/top-left-arrows.svg`,
topRight: `${origin}/arrows/top-right-arrow.svg`,
bottomLeft: `${origin}/arrows/bottom-left-arrow.svg`,
bottomRight: `${origin}/arrows/bottom-right-arrow.svg`,
}

const link: PaymentLink & { token?: string } = {
type,
username,
Expand Down Expand Up @@ -149,14 +174,6 @@ export async function GET(req: NextRequest) {
)
}

// create an object with all arrow SVG paths
const arrowSrcs = {
topLeft: `${origin}/arrows/top-left-arrows.svg`,
topRight: `${origin}/arrows/top-right-arrow.svg`,
bottomLeft: `${origin}/arrows/bottom-left-arrow.svg`,
bottomRight: `${origin}/arrows/bottom-right-arrow.svg`,
}

const link: PaymentLink & { token?: string } = {
type,
username,
Expand Down
Loading
Loading