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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@zerodev/sdk": "5.5.0",
"auto-text-size": "^0.2.3",
"autoprefixer": "^10.4.20",
"canvas-confetti": "^1.9.3",
"chakra-ui-steps": "^2.1.0",
"classnames": "^2.5.1",
"embla-carousel-react": "^8.6.0",
Expand Down Expand Up @@ -92,6 +93,7 @@
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/canvas-confetti": "^1.9.0",
"@types/chroma-js": "^2.4.4",
"@types/jest": "^29.5.12",
"@types/js-cookie": "^3.0.6",
Expand Down
326 changes: 172 additions & 154 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

93 changes: 81 additions & 12 deletions src/app/(mobile-ui)/points/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,100 @@ import TransactionAvatarBadge from '@/components/TransactionDetails/TransactionA
import { VerifiedUserLabel } from '@/components/UserHeader'
import { useAuth } from '@/context/authContext'
import { invitesApi } from '@/services/invites'
import { Invite } from '@/services/services.types'
import { PointsInvite } from '@/services/services.types'
import { useQuery } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'
import STAR_STRAIGHT_ICON from '@/assets/icons/starStraight.svg'
import Image from 'next/image'
import { pointsApi } from '@/services/points'
import EmptyState from '@/components/Global/EmptyStates/EmptyState'
import { getInitialsFromName } from '@/utils'

const PointsPage = () => {
const router = useRouter()
const { user } = useAuth()
const { data: invites, isLoading } = useQuery({
const {
data: invites,
isLoading,
isError: isInvitesError,
error: invitesError,
} = useQuery({
queryKey: ['invites', user?.user.userId],
queryFn: () => invitesApi.getInvites(),
enabled: !!user?.user.userId,
})

const {
data: tierInfo,
isLoading: isTierInfoLoading,
isError: isTierInfoError,
error: tierInfoError,
} = useQuery({
queryKey: ['tierInfo', user?.user.userId],
queryFn: () => pointsApi.getTierInfo(),
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 />
if (isLoading || isTierInfoLoading) {
return <PeanutLoading />
}

if (isInvitesError || isTierInfoError) {
console.error('Error loading points data:', invitesError ?? tierInfoError)

return (
<div className="mx-auto mt-6 w-full space-y-3 md:max-w-2xl">
<EmptyState icon="alert" title="Error loading points!" description="Please contact Support." />
</div>
)
}

return (
<PageContainer className="flex flex-col">
<NavHeader title="Invites" onPrev={() => router.back()} />

<section className="mx-auto mb-auto mt-10 w-full space-y-4">
{tierInfo?.data && (
<Card className="flex flex-col items-center justify-center gap-2 p-4">
<h2 className="text-3xl font-extrabold text-black">TIER {tierInfo?.data.currentTier}</h2>
<span className="flex items-center gap-2">
<Image src={STAR_STRAIGHT_ICON} alt="star" width={20} height={20} />
{tierInfo.data.totalPoints} Points
</span>

{/* Progress bar */}
<div className="flex w-full items-center gap-2">
{tierInfo?.data.currentTier}
<div className="w-full">
<div className="relative h-1.5 w-full overflow-hidden rounded-full bg-grey-2">
<div
className="h-full rounded-full bg-primary-1 transition-all duration-300"
style={{
width: `${(tierInfo.data.totalPoints / tierInfo.data.nextTierThreshold) * 100}%`,
}}
/>
</div>
</div>
{tierInfo?.data.currentTier + 1}
</div>

<p className="text-sm text-grey-1">
{tierInfo.data.pointsToNextTier} points needed for the next tier
</p>
</Card>
)}

<div className="mx-3 flex items-center gap-2">
<Icon name="info" className="size-6 text-black md:size-3" />

<p className="text-sm text-black">
Do stuff on Peanut and get points. Invite friends and pocket 20% of their points, too.
</p>
</div>

<h1 className="font-bold">Refer friends</h1>
<div className="flex w-full items-center justify-between gap-3">
<Card className="flex w-1/2 items-center justify-center py-3.5">
Expand All @@ -46,7 +114,7 @@ const PointsPage = () => {
<CopyToClipboard type="button" textToCopy={inviteCode} />
</div>

{invites?.length && invites.length > 0 && (
{invites?.invitees?.length && invites.invitees.length > 0 && (
<>
<ShareButton
generateText={() =>
Expand All @@ -60,26 +128,27 @@ const PointsPage = () => {
</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'
{invites.invitees?.map((invite: PointsInvite, i: number) => {
const username = invite.username
const fullName = invite.fullName
const isVerified = invite.kycStatus === 'approved'
return (
<Card key={invite.id} position={getCardPosition(i, invites.length)}>
<Card key={invite.inviteeId} position={getCardPosition(i, invites.invitees.length)}>
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<TransactionAvatarBadge
initials={username}
initials={getInitialsFromName(fullName ?? 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>
<p className="text-grey-1">+{invite.totalPoints} pts</p>
</div>
</Card>
)
Expand All @@ -88,7 +157,7 @@ const PointsPage = () => {
</>
)}

{invites?.length === 0 && (
{invites?.invitees?.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" />
Expand Down
22 changes: 21 additions & 1 deletion src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ import { useWallet } from '@/hooks/wallet/useWallet'
import { AccountType, Account } from '@/interfaces'
import { formatIban, shortenAddressLong, isTxReverted } from '@/utils/general.utils'
import { useParams, useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import DirectSuccessView from '@/components/Payment/Views/Status.payment.view'
import { ErrorHandler, getBridgeChainName } from '@/utils'
import { getOfframpCurrencyConfig } from '@/utils/bridge.utils'
import { createOfframp, confirmOfframp } from '@/app/actions/offramp'
import { useAuth } from '@/context/authContext'
import ExchangeRate from '@/components/ExchangeRate'
import countryCurrencyMappings from '@/constants/countryCurrencyMapping'
import { useQuery } from '@tanstack/react-query'
import { pointsApi } from '@/services/points'
import { PointsAction } from '@/services/services.types'

type View = 'INITIAL' | 'SUCCESS'

Expand All @@ -40,6 +43,22 @@ export default function WithdrawBankPage() {
currency.path?.toLowerCase() === country.toLowerCase()
)?.currencyCode

const queryKey = useMemo(
() => ['calculate-points', 'withdraw', bankAccount?.id, amountToWithdraw],
[bankAccount?.id, amountToWithdraw]
)

// Calculate points API call
const { data: pointsData } = useQuery({
queryKey,
queryFn: () =>
pointsApi.calculatePoints({
actionType: PointsAction.BRIDGE_TRANSFER,
usdAmount: Number(amountToWithdraw),
}),
refetchOnWindowFocus: false,
})

useEffect(() => {
if (!bankAccount) {
router.replace('/withdraw')
Expand Down Expand Up @@ -272,6 +291,7 @@ export default function WithdrawBankPage() {
isWithdrawFlow
currencyAmount={`$${amountToWithdraw}`}
message={bankAccount ? shortenAddressLong(bankAccount.identifier.toUpperCase()) : ''}
points={pointsData?.estimatedPoints}
/>
)}
</div>
Expand Down
19 changes: 19 additions & 0 deletions src/app/[...recipient]/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import NavHeader from '@/components/Global/NavHeader'
import { ReqFulfillBankFlowManager } from '@/components/Request/views/ReqFulfillBankFlowManager'
import SupportCTA from '@/components/Global/SupportCTA'
import { BankRequestType, useDetermineBankRequestType } from '@/hooks/useDetermineBankRequestType'
import { useQuery } from '@tanstack/react-query'
import { pointsApi } from '@/services/points'
import { PointsAction } from '@/services/services.types'

export type PaymentFlow = 'request_pay' | 'external_wallet' | 'direct_pay' | 'withdraw'
interface Props {
Expand Down Expand Up @@ -70,6 +73,21 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props)
} = useRequestFulfillmentFlow()
const { requestType } = useDetermineBankRequestType(chargeDetails?.requestLink.recipientAccount.userId ?? '')

// Calculate points API call
const { data: pointsData } = useQuery({
queryKey: ['calculate-points', chargeDetails?.uuid],
queryFn: () =>
pointsApi.calculatePoints({
actionType: PointsAction.P2P_REQUEST_PAYMENT,
usdAmount: Number(usdAmount),
otherUserId: chargeDetails?.requestLink.recipientAccount.userId,
}),
// Pre fetch points when in confirm view.
// Fetch only for logged in users.
enabled: !!(user?.user.userId && usdAmount && currentView === 'CONFIRM' && flow === 'request_pay'),
refetchOnWindowFocus: false,
})
Comment thread
Zishan-7 marked this conversation as resolved.
Comment thread
Zishan-7 marked this conversation as resolved.

// determine if the current user is the recipient of the transaction
const isCurrentUserRecipient = chargeDetails?.requestLink.recipientAccount?.userId === user?.user.userId

Expand Down Expand Up @@ -536,6 +554,7 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props)
}
isExternalWalletFlow={isExternalWalletFlow}
redirectTo={isExternalWalletFlow ? '/add-money' : '/send'}
points={pointsData?.estimatedPoints}
/>
)}
</>
Expand Down
46 changes: 44 additions & 2 deletions src/components/Claim/Link/Onchain/Success.view.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client'
import { Button } from '@/components/0_Bruddle'
import NavHeader from '@/components/Global/NavHeader'
import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard'
Expand All @@ -8,15 +9,19 @@ import { useClaimBankFlow } from '@/context/ClaimBankFlowContext'
import { useUserStore } from '@/redux/hooks'
import { ESendLinkStatus, sendLinksApi } from '@/services/sendLinks'
import { formatTokenAmount, getTokenDetails, printableAddress, shortenAddressLong } from '@/utils'
import { useQueryClient } from '@tanstack/react-query'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'
import { useEffect, useMemo } from 'react'
import type { Hash } from 'viem'
import { formatUnits } from 'viem'
import * as _consts from '../../Claim.consts'
import Image from 'next/image'
import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets'
import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO, STAR_STRAIGHT_ICON } from '@/assets'
import CreateAccountButton from '@/components/Global/CreateAccountButton'
import { pointsApi } from '@/services/points'
import { PointsAction } from '@/services/services.types'
import PeanutLoading from '@/components/Global/PeanutLoading'
import { shootDoubleStarConfetti } from '@/utils/confetti'

export const SuccessClaimLinkView = ({
transactionHash,
Expand All @@ -31,6 +36,25 @@ export const SuccessClaimLinkView = ({
const queryClient = useQueryClient()
const { offrampDetails, claimType, bankDetails } = useClaimBankFlow()

const queryKey = useMemo(() => ['calculate-points', 'claim-link', claimLinkData.link], [claimLinkData.link])

const { data: pointsData, isLoading: isPointsDataLoading } = useQuery({
queryKey,
queryFn: () =>
pointsApi.calculatePoints({
actionType: PointsAction.P2P_SEND_LINK,
usdAmount: Number(
formatTokenAmount(
Number(formatUnits(claimLinkData.amount, claimLinkData.tokenDecimals)) * (tokenPrice ?? 0)
)
),
otherUserId: claimLinkData?.senderAddress,
}),
Comment thread
Zishan-7 marked this conversation as resolved.
// Fetch only for logged in users.
enabled: !!authUser?.user.userId,
refetchOnWindowFocus: false,
})

useEffect(() => {
queryClient.invalidateQueries({ queryKey: [TRANSACTIONS] })
}, [queryClient])
Expand Down Expand Up @@ -117,6 +141,12 @@ export const SuccessClaimLinkView = ({
title: isBankClaim ? 'You will receive' : 'You claimed',
}

useEffect(() => {
if (pointsData?.estimatedPoints) {
shootDoubleStarConfetti({ origin: { x: 0.5, y: 0.6 } })
}
}, [pointsData?.estimatedPoints])

const renderButtons = () => {
if (authUser?.user.userId) {
return (
Expand All @@ -135,6 +165,10 @@ export const SuccessClaimLinkView = ({
return <CreateAccountButton onClick={() => router.push('/setup')} />
}

if (isPointsDataLoading) {
return <PeanutLoading />
}
Comment thread
Zishan-7 marked this conversation as resolved.

return (
<div className="flex min-h-[inherit] flex-col justify-between gap-8">
<SoundPlayer sound="success" />
Expand All @@ -149,6 +183,14 @@ export const SuccessClaimLinkView = ({
</div>
<div className="my-auto flex h-full flex-col justify-center space-y-4">
<PeanutActionDetailsCard {...cardProps} />
{pointsData && (
<div className="flex justify-center gap-2">
<Image src={STAR_STRAIGHT_ICON} alt="star" width={20} height={20} />
<p className="text-sm font-medium text-black">
You&apos;ve earned {pointsData.estimatedPoints} points!
</p>
</div>
)}
{renderButtons()}
</div>
</div>
Expand Down
Loading
Loading