Skip to content
24 changes: 15 additions & 9 deletions src/app/(mobile-ui)/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,10 @@ import { useDeviceType, DeviceType } from '@/hooks/useGetDeviceType'
import SetupNotificationsModal from '@/components/Notifications/SetupNotificationsModal'
import { useNotifications } from '@/hooks/useNotifications'
import useKycStatus from '@/hooks/useKycStatus'
import HomeBanners from '@/components/Home/HomeBanners'
import HomeCarouselCTA from '@/components/Home/HomeCarouselCTA'
import NoMoreJailModal from '@/components/Global/NoMoreJailModal'
import EarlyUserModal from '@/components/Global/EarlyUserModal'
import { STAR_STRAIGHT_ICON } from '@/assets'
import Image from 'next/image'
import InvitesIcon from '@/components/Home/InvitesIcon'
import NavigationArrow from '@/components/Global/NavigationArrow'

const BALANCE_WARNING_THRESHOLD = parseInt(process.env.NEXT_PUBLIC_BALANCE_WARNING_THRESHOLD ?? '500')
Expand Down Expand Up @@ -199,9 +198,9 @@ export default function Home() {
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

if (shouldShow) {
if (shouldShow && !showAddMoneyPromptModal) {
// Only set state, don't set sessionStorage here to avoid race conditions
setShowAddMoneyPromptModal(true)
sessionStorage.setItem('hasSeenAddMoneyPromptThisSession', 'true')
} else if (showAddMoneyPromptModal && showPermissionModal) {
// priority enforcement: hide add money modal if notification modal appears
// this handles race conditions where both modals try to show simultaneously
Expand All @@ -219,6 +218,13 @@ export default function Home() {
address,
])

// Set sessionStorage flag when modal becomes visible to prevent showing again
useEffect(() => {
if (showAddMoneyPromptModal) {
sessionStorage.setItem('hasSeenAddMoneyPromptThisSession', 'true')
}
}, [showAddMoneyPromptModal])

if (isLoading) {
return <PeanutLoading coverFullScreen />
}
Expand All @@ -228,9 +234,9 @@ 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} />
<Link href="/points" className="flex items-center gap-2">
<Image src={STAR_STRAIGHT_ICON} alt="star" width={20} height={20} />
<span className="whitespace-nowrap text-sm font-semibold md:text-base">Points</span>
<Link href="/points" className="flex items-center gap-0">
<InvitesIcon />
<span className="whitespace-nowrap pl-1 text-sm font-semibold md:text-base">Points</span>
<NavigationArrow size={16} className="fill-black" />
</Link>
{/* <NotificationNavigation /> */}
Expand Down Expand Up @@ -260,7 +266,7 @@ export default function Home() {
</ActionButtonGroup>
</div>

<HomeBanners />
<HomeCarouselCTA />

{showPermissionModal && <SetupNotificationsModal />}

Expand Down
49 changes: 47 additions & 2 deletions src/app/(mobile-ui)/qr-pay/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import TokenAmountInput from '@/components/Global/TokenAmountInput'
import { useWallet } from '@/hooks/wallet/useWallet'
import { clearRedirectUrl, getRedirectUrl, isTxReverted, saveRedirectUrl, formatNumberForDisplay } from '@/utils'
import { getShakeClass, type ShakeIntensity } from '@/utils/perk.utils'
import { calculateSavingsInCents, isArgentinaMantecaQrPayment, getSavingsMessage } from '@/utils/qr-payment.utils'
import ErrorAlert from '@/components/Global/ErrorAlert'
import { PEANUT_WALLET_TOKEN_DECIMALS, TRANSACTIONS, PERK_HOLD_DURATION_MS } from '@/constants'
import { MANTECA_DEPOSIT_ADDRESS } from '@/constants/manteca.consts'
Expand All @@ -41,6 +42,9 @@ import { useQueryClient } from '@tanstack/react-query'
import { shootDoubleStarConfetti } from '@/utils/confetti'
import { STAR_STRAIGHT_ICON } from '@/assets'
import { useAuth } from '@/context/authContext'
import { PointsAction } from '@/services/services.types'
import { usePointsConfetti } from '@/hooks/usePointsConfetti'
import { usePointsCalculation } from '@/hooks/usePointsCalculation'
import { useWebSocket } from '@/hooks/useWebSocket'
import type { HistoryEntry } from '@/hooks/useTransactionHistory'
import { completeHistoryEntry } from '@/utils/history.utils'
Expand Down Expand Up @@ -313,6 +317,17 @@ export default function QRPayPage() {
}
}, [paymentProcessor, simpleFiPayment, paymentLock?.code, paymentLock?.paymentAgainstAmount, amount])

// Fetch points early to avoid latency penalty - fetch as soon as we have usdAmount
// This way points are cached by the time success view shows
// Only Manteca QR payments give points (SimpleFi does not)
// Use timestamp as uniqueId to prevent cache collisions between different QR scans
const { pointsData, pointsDivRef } = usePointsCalculation(
PointsAction.MANTECA_QR_PAYMENT,
usdAmount,
paymentProcessor === 'MANTECA',
timestamp || undefined
)

const methodIcon = useMemo(() => {
switch (qrType) {
case EQrType.MERCADO_PAGO:
Expand Down Expand Up @@ -716,6 +731,12 @@ export default function QRPayPage() {

// Check user balance
useEffect(() => {
// Skip balance check on success screen (balance may not have updated yet)
if (isSuccess) {
setBalanceErrorMessage(null)
return
}

// Skip balance check if transaction is being processed
if (hasPendingTransactions || isWaitingForWebSocket) {
return
Expand All @@ -733,13 +754,16 @@ export default function QRPayPage() {
} else {
setBalanceErrorMessage(null)
}
}, [usdAmount, balance, hasPendingTransactions, isWaitingForWebSocket])
}, [usdAmount, balance, hasPendingTransactions, isWaitingForWebSocket, isSuccess])

// Use points confetti hook for animation - must be called unconditionally
usePointsConfetti(isSuccess && pointsData?.estimatedPoints ? pointsData.estimatedPoints : undefined, pointsDivRef)

useEffect(() => {
if (isSuccess) {
queryClient.invalidateQueries({ queryKey: [TRANSACTIONS] })
}
}, [isSuccess])
}, [isSuccess, queryClient])

const handleSimplefiRetry = useCallback(async () => {
setShowOrderNotReadyModal(false)
Expand Down Expand Up @@ -947,6 +971,11 @@ export default function QRPayPage() {
if (isSuccess && paymentProcessor === 'MANTECA' && !qrPayment) {
return null
} else if (isSuccess && paymentProcessor === 'MANTECA' && qrPayment) {
// Calculate savings for Argentina Manteca QR payments only
const savingsInCents = calculateSavingsInCents(usdAmount)
const showSavingsMessage = savingsInCents > 0 && isArgentinaMantecaQrPayment(qrType, paymentProcessor)
const savingsMessage = showSavingsMessage ? getSavingsMessage(savingsInCents) : ''

return (
<div className={`flex min-h-[inherit] flex-col gap-8 ${getShakeClass(isShaking, shakeIntensity)}`}>
<SoundPlayer sound="success" />
Expand Down Expand Up @@ -976,6 +1005,10 @@ export default function QRPayPage() {
<div className="text-lg font-bold">
≈ {formatNumberForDisplay(usdAmount ?? undefined, { maxDecimals: 2 })} USD
</div>
{/* Savings Message (Argentina Manteca only) */}
{showSavingsMessage && savingsMessage && (
<p className="text-sm italic text-grey-1">{savingsMessage}</p>
)}
</div>
</Card>
)}
Expand Down Expand Up @@ -1028,6 +1061,17 @@ export default function QRPayPage() {
</Card>
)}

{/* Points Display - ref used for confetti origin point */}
{pointsData?.estimatedPoints && (
<div ref={pointsDivRef} 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}{' '}
{pointsData.estimatedPoints === 1 ? 'point' : 'points'}!
</p>
</div>
)}

<div className="w-full space-y-5">
{/* Show Claim Perk button if eligible and not claimed yet */}
{qrPayment?.perk?.eligible && !perkClaimed && !qrPayment.perk.claimed ? (
Expand Down Expand Up @@ -1153,6 +1197,7 @@ export default function QRPayPage() {
</div>
</div>
</Card>

<div className="w-full space-y-5">
<Button
onClick={() => {
Expand Down
35 changes: 31 additions & 4 deletions src/app/(mobile-ui)/support/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
'use client'

import { useState, useEffect } from 'react'
import { useCrispUserData } from '@/hooks/useCrispUserData'
import { useCrispProxyUrl } from '@/hooks/useCrispProxyUrl'
import PeanutLoading from '@/components/Global/PeanutLoading'

const SupportPage = () => {
const userData = useCrispUserData()
const crispProxyUrl = useCrispProxyUrl(userData)
const [isLoading, setIsLoading] = useState(true)

useEffect(() => {
// Listen for ready message from proxy iframe
const handleMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) return

if (event.data.type === 'CRISP_READY') {
setIsLoading(false)
}
}

window.addEventListener('message', handleMessage)
return () => window.removeEventListener('message', handleMessage)
}, [])

return (
<iframe
src="https://go.crisp.chat/chat/embed/?website_id=916078be-a6af-4696-82cb-bc08d43d9125"
className="h-full w-full md:max-w-[90%] md:pl-24"
/>
<div className="relative h-full w-full md:max-w-[90%] md:pl-24">
{isLoading && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-background">
<PeanutLoading />
</div>
)}
<iframe src={crispProxyUrl} className="h-full w-full" title="Support Chat" />
</div>
)
}

Expand Down
19 changes: 7 additions & 12 deletions src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ 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'
import { usePointsCalculation } from '@/hooks/usePointsCalculation'
import { useSearchParams } from 'next/navigation'
import { parseUnits } from 'viem'

Expand Down Expand Up @@ -61,16 +60,12 @@ export default function WithdrawBankPage() {
)?.currencyCode

// Calculate points API call
const { data: pointsData } = useQuery({
queryKey: ['calculate-points', 'withdraw', bankAccount?.id, amountToWithdraw],
queryFn: () =>
pointsApi.calculatePoints({
actionType: PointsAction.BRIDGE_TRANSFER,
usdAmount: Number(amountToWithdraw),
}),
enabled: !!(user?.user.userId && amountToWithdraw && bankAccount),
refetchOnWindowFocus: false,
})
const { pointsData } = usePointsCalculation(
PointsAction.BRIDGE_TRANSFER,
amountToWithdraw,
!!(amountToWithdraw && bankAccount),
bankAccount?.id
)

// non-eur sepa countries that are currently experiencing issues
const isNonEuroSepaCountry = !!(
Expand Down
28 changes: 26 additions & 2 deletions src/app/(mobile-ui)/withdraw/manteca/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { useWallet } from '@/hooks/wallet/useWallet'
import { useState, useMemo, useContext, useEffect, useCallback } from 'react'
import { useState, useMemo, useContext, useEffect, useCallback, useRef, useId } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { Button } from '@/components/0_Bruddle/Button'
import { Card } from '@/components/0_Bruddle/Card'
Expand Down Expand Up @@ -41,13 +41,18 @@ import { useQueryClient } from '@tanstack/react-query'
import { captureException } from '@sentry/nextjs'
import useKycStatus from '@/hooks/useKycStatus'
import { usePendingTransactions } from '@/hooks/wallet/usePendingTransactions'
import { PointsAction } from '@/services/services.types'
import { usePointsConfetti } from '@/hooks/usePointsConfetti'
import { usePointsCalculation } from '@/hooks/usePointsCalculation'
import STAR_STRAIGHT_ICON from '@/assets/icons/starStraight.svg'

type MantecaWithdrawStep = 'amountInput' | 'bankDetails' | 'review' | 'success' | 'failure'

const MAX_WITHDRAW_AMOUNT = '2000'
const MIN_WITHDRAW_AMOUNT = '1'

export default function MantecaWithdrawFlow() {
const flowId = useId() // Unique ID per flow instance to prevent cache collisions
const [amount, setAmount] = useState<string | undefined>(undefined)
const [currencyAmount, setCurrencyAmount] = useState<string | undefined>(undefined)
const [usdAmount, setUsdAmount] = useState<string | undefined>(undefined)
Expand Down Expand Up @@ -290,11 +295,18 @@ export default function MantecaWithdrawFlow() {
}
}, [usdAmount, balance, hasPendingTransactions])

// Fetch points early to avoid latency penalty - fetch as soon as we have usdAmount
// Use flowId as uniqueId to prevent cache collisions between different withdrawal flows
const { pointsData, pointsDivRef } = usePointsCalculation(PointsAction.MANTECA_TRANSFER, usdAmount, true, flowId)

// Use points confetti hook for animation - must be called unconditionally
usePointsConfetti(step === 'success' ? pointsData?.estimatedPoints : undefined, pointsDivRef)

useEffect(() => {
if (step === 'success') {
queryClient.invalidateQueries({ queryKey: [TRANSACTIONS] })
}
}, [step])
}, [step, queryClient])

if (isCurrencyLoading || !currencyPrice || !selectedCountry) {
return <PeanutLoading />
Expand Down Expand Up @@ -323,6 +335,18 @@ export default function MantecaWithdrawFlow() {
<h1 className="text-sm font-normal text-grey-1">to {destinationAddress}</h1>
</div>
</Card>

{/* Points Display - ref used for confetti origin point */}
{pointsData?.estimatedPoints && (
<div ref={pointsDivRef} className="flex justify-center gap-2">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qn non blocking: why is ref neded?

<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}{' '}
{pointsData.estimatedPoints === 1 ? 'point' : 'points'}!
</p>
</div>
)}

<div className="w-full space-y-5">
<Button
onClick={() => {
Expand Down
22 changes: 8 additions & 14 deletions src/app/[...recipient]/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ 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'
import { usePointsCalculation } from '@/hooks/usePointsCalculation'

export type PaymentFlow = 'request_pay' | 'external_wallet' | 'direct_pay' | 'withdraw'
interface Props {
Expand Down Expand Up @@ -81,22 +80,17 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props)
// For direct_pay: calculate on STATUS (after payment completes)

const shouldFetchPoints =
user?.user.userId &&
usdAmount &&
chargeDetails?.uuid &&
((flow === 'request_pay' && currentView === 'CONFIRM') || (flow === 'direct_pay' && currentView === 'STATUS'))

const { data: pointsData } = useQuery({
queryKey: ['calculate-points', chargeDetails?.uuid, flow],
queryFn: () =>
pointsApi.calculatePoints({
actionType: PointsAction.P2P_REQUEST_PAYMENT,
usdAmount: Number(usdAmount),
otherUserId: chargeDetails?.requestLink.recipientAccount.userId,
}),
enabled: !!shouldFetchPoints,
refetchOnWindowFocus: false,
})
const { pointsData } = usePointsCalculation(
PointsAction.P2P_REQUEST_PAYMENT,
usdAmount,
!!shouldFetchPoints,
chargeDetails?.uuid,
chargeDetails?.requestLink.recipientAccount.userId
)

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