From 332f17997efeef3b4741a7938801fe58dad22e1f Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Fri, 31 Oct 2025 20:43:25 -0300 Subject: [PATCH 01/17] various fixes --- src/app/(mobile-ui)/home/page.tsx | 15 +- src/app/(mobile-ui)/qr-pay/page.tsx | 80 +++++++++- src/app/(mobile-ui)/withdraw/manteca/page.tsx | 39 ++++- src/components/Global/Slider/index.tsx | 2 +- .../Home/HomeBanners/BannerCard.tsx | 51 ------- src/components/Home/HomeBanners/index.tsx | 51 ------- .../Home/HomeCarouselCTA/CarouselCTA.tsx | 143 ++++++++++++++++++ src/components/Home/HomeCarouselCTA/index.tsx | 41 +++++ .../Profile/components/ProfileHeader.tsx | 22 +-- src/constants/carousel.consts.ts | 4 + src/constants/index.ts | 1 + ...useBanners.tsx => useHomeCarouselCTAs.tsx} | 46 +++--- src/utils/general.utils.ts | 20 ++- src/utils/qr-payment.utils.ts | 49 ++++++ 14 files changed, 419 insertions(+), 145 deletions(-) delete mode 100644 src/components/Home/HomeBanners/BannerCard.tsx delete mode 100644 src/components/Home/HomeBanners/index.tsx create mode 100644 src/components/Home/HomeCarouselCTA/CarouselCTA.tsx create mode 100644 src/components/Home/HomeCarouselCTA/index.tsx create mode 100644 src/constants/carousel.consts.ts rename src/hooks/{useBanners.tsx => useHomeCarouselCTAs.tsx} (68%) create mode 100644 src/utils/qr-payment.utils.ts diff --git a/src/app/(mobile-ui)/home/page.tsx b/src/app/(mobile-ui)/home/page.tsx index ec161d207..9d446f190 100644 --- a/src/app/(mobile-ui)/home/page.tsx +++ b/src/app/(mobile-ui)/home/page.tsx @@ -33,7 +33,7 @@ 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' @@ -199,9 +199,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 @@ -219,6 +219,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 } @@ -260,7 +267,7 @@ export default function Home() { - + {showPermissionModal && } diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index daddc3cf6..351defd9a 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -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' @@ -37,10 +38,13 @@ import { QrKycState, useQrKycGate } from '@/hooks/useQrKycGate' import ActionModal from '@/components/Global/ActionModal' import { MantecaGeoSpecificKycModal } from '@/components/Kyc/InitiateMantecaKYCModal' import { SoundPlayer } from '@/components/Global/SoundPlayer' -import { useQueryClient } from '@tanstack/react-query' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { shootDoubleStarConfetti } from '@/utils/confetti' import { STAR_STRAIGHT_ICON } from '@/assets' import { useAuth } from '@/context/authContext' +import { pointsApi } from '@/services/points' +import { PointsAction } from '@/services/services.types' +import { usePointsConfetti } from '@/hooks/usePointsConfetti' import { useWebSocket } from '@/hooks/useWebSocket' import type { HistoryEntry } from '@/hooks/useTransactionHistory' import { completeHistoryEntry } from '@/utils/history.utils' @@ -92,6 +96,7 @@ export default function QRPayPage() { const { setIsSupportModalOpen } = useSupportModalContext() const [waitingForMerchantAmount, setWaitingForMerchantAmount] = useState(false) const retryCount = useRef(0) + const pointsDivRef = useRef(null) const paymentProcessor: PaymentProcessor | null = useMemo(() => { switch (qrType) { @@ -313,6 +318,20 @@ 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 + const { data: pointsData } = useQuery({ + queryKey: ['calculate-points', 'qr-payment', paymentProcessor, usdAmount], + queryFn: () => + pointsApi.calculatePoints({ + actionType: PointsAction.MANTECA_QR_PAYMENT, // Both Manteca and SimpleFi QR payments use this type + usdAmount: Number(usdAmount), + }), + enabled: !!(user?.user.userId && usdAmount && Number(usdAmount) > 0 && paymentProcessor), + refetchOnWindowFocus: false, + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + }) + const methodIcon = useMemo(() => { switch (qrType) { case EQrType.MERCADO_PAGO: @@ -735,11 +754,14 @@ export default function QRPayPage() { } }, [usdAmount, balance, hasPendingTransactions, isWaitingForWebSocket]) + // 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) @@ -947,6 +969,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 (
@@ -980,6 +1007,22 @@ export default function QRPayPage() { )} + {/* Savings Message - Show after payment card (Argentina Manteca only) */} + {showSavingsMessage && savingsMessage && ( +

{savingsMessage}

+ )} + + {/* Points Display - Show after payment card */} + {pointsData?.estimatedPoints && ( +
+ star +

+ You've earned {pointsData.estimatedPoints}{' '} + {pointsData.estimatedPoints === 1 ? 'point' : 'points'}! +

+
+ )} + {/* Perk Eligibility Card - Show before claiming */} {qrPayment?.perk?.eligible && !perkClaimed && !qrPayment.perk.claimed && ( @@ -1028,6 +1071,27 @@ export default function QRPayPage() { )} + {/* Savings Message and Points - Show after perk banner if perk claimed */} + {(perkClaimed || qrPayment?.perk?.claimed) && ( + <> + {/* Savings Message (Argentina Manteca only) */} + {showSavingsMessage && savingsMessage && ( +

{savingsMessage}

+ )} + + {/* Points Display */} + {pointsData?.estimatedPoints && ( +
+ star +

+ You've earned {pointsData.estimatedPoints}{' '} + {pointsData.estimatedPoints === 1 ? 'point' : 'points'}! +

+
+ )} + + )} +
{/* Show Claim Perk button if eligible and not claimed yet */} {qrPayment?.perk?.eligible && !perkClaimed && !qrPayment.perk.claimed ? ( @@ -1153,6 +1217,18 @@ export default function QRPayPage() {
+ + {/* Points Display */} + {pointsData?.estimatedPoints && ( +
+ star +

+ You've earned {pointsData.estimatedPoints}{' '} + {pointsData.estimatedPoints === 1 ? 'point' : 'points'}! +

+
+ )} +
+ + {/* Points Display */} + {pointsData?.estimatedPoints && ( +
+ star +

+ You've earned {pointsData.estimatedPoints}{' '} + {pointsData.estimatedPoints === 1 ? 'point' : 'points'}! +

+
+ )} +
+ + {/* Icon container */} +
+ {/* Show icon only if logo isn't provided. Logo takes precedence over icon. */} + {!logo && } + {logo && {typeof} +
+ + {/* Content */} +
+

{title}

+

{description}

+
+ + + {/* Permission denied modal for notification CTA */} + {isPermissionDenied && showPermissionDeniedModal && ( + + To keep getting updates, you'll need to reinstall the app on your home screen. +
+ Don't worry, your account and data are safe. It only takes a minute. +

+ } + icon="bell" + onClose={() => { + setShowPermissionDeniedModal(false) + }} + ctaClassName="md:flex-col gap-4" + ctas={[ + { + text: 'Got it!', + onClick: () => { + setShowPermissionDeniedModal(false) + }, + shadowSize: '4', + variant: 'purple', + }, + { + text: 'Reinstall later', + onClick: () => { + setShowPermissionDeniedModal(false) + }, + variant: 'transparent', + className: 'underline h-6', + }, + ]} + /> + )} + + ) +} + +export default CarouselCTA + diff --git a/src/components/Home/HomeCarouselCTA/index.tsx b/src/components/Home/HomeCarouselCTA/index.tsx new file mode 100644 index 000000000..15e8a5751 --- /dev/null +++ b/src/components/Home/HomeCarouselCTA/index.tsx @@ -0,0 +1,41 @@ +'use client' + +import Carousel from '@/components/Global/Carousel' +import CarouselCTA from './CarouselCTA' +import { type IconName } from '@/components/Global/Icons/Icon' +import { useHomeCarouselCTAs } from '@/hooks/useHomeCarouselCTAs' + +const HomeCarouselCTA = () => { + const { carouselCTAs, setCarouselCTAs } = useHomeCarouselCTAs() + + // don't render carousel if there are no CTAs + if (!carouselCTAs.length) return null + + return ( + + {carouselCTAs.map((cta) => ( + { + // Use cta.onClose if provided (for notification prompt), otherwise filter from list + if (cta.onClose) { + cta.onClose() + } else { + setCarouselCTAs((prev) => prev.filter((c) => c.id !== cta.id)) + } + }} + onClick={cta.onClick} + logo={cta.logo} + iconContainerClassName={cta.iconContainerClassName} + isPermissionDenied={cta.isPermissionDenied} + /> + ))} + + ) +} + +export default HomeCarouselCTA + diff --git a/src/components/Profile/components/ProfileHeader.tsx b/src/components/Profile/components/ProfileHeader.tsx index 333f6a366..b89d385b8 100644 --- a/src/components/Profile/components/ProfileHeader.tsx +++ b/src/components/Profile/components/ProfileHeader.tsx @@ -11,6 +11,7 @@ import { Drawer, DrawerContent, DrawerTitle } from '@/components/Global/Drawer' import { VerifiedUserLabel } from '@/components/UserHeader' import { useAuth } from '@/context/authContext' import useKycStatus from '@/hooks/useKycStatus' +import CopyToClipboard from '@/components/Global/CopyToClipboard' interface ProfileHeaderProps { name: string @@ -44,15 +45,18 @@ const ProfileHeader: React.FC = ({ {/* Name */} - +
+ + +
{/* Username with share drawer */} {showShareButton && ( @@ -88,7 +91,9 @@ const CarouselCTA = ({ > {/* Show icon only if logo isn't provided. Logo takes precedence over icon. */} {!logo && } - {logo && {typeof} + {logo && ( + {typeof + )}
{/* Content */} @@ -140,4 +145,3 @@ const CarouselCTA = ({ } export default CarouselCTA - diff --git a/src/components/Home/HomeCarouselCTA/index.tsx b/src/components/Home/HomeCarouselCTA/index.tsx index 15e8a5751..e7ad1bf00 100644 --- a/src/components/Home/HomeCarouselCTA/index.tsx +++ b/src/components/Home/HomeCarouselCTA/index.tsx @@ -38,4 +38,3 @@ const HomeCarouselCTA = () => { } export default HomeCarouselCTA - diff --git a/src/constants/carousel.consts.ts b/src/constants/carousel.consts.ts index 42681e06c..2d4807569 100644 --- a/src/constants/carousel.consts.ts +++ b/src/constants/carousel.consts.ts @@ -1,4 +1,3 @@ // Carousel CTA styling constants export const CAROUSEL_CLOSE_BUTTON_POSITION = 'absolute right-2 top-2' export const CAROUSEL_CLOSE_ICON_SIZE = 10 - From db501818fa899f3bfea0ac7bcfd5fbd04550cdcb Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Fri, 31 Oct 2025 21:22:22 -0300 Subject: [PATCH 03/17] added support cta --- src/components/Common/ActionList.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Common/ActionList.tsx b/src/components/Common/ActionList.tsx index 6ec704230..4b419b754 100644 --- a/src/components/Common/ActionList.tsx +++ b/src/components/Common/ActionList.tsx @@ -33,6 +33,7 @@ import { useWallet } from '@/hooks/wallet/useWallet' import { ActionListCard } from '../ActionListCard' import { useGeoFilteredPaymentOptions } from '@/hooks/useGeoFilteredPaymentOptions' import { tokenSelectorContext } from '@/context' +import SupportCTA from '../Global/SupportCTA' interface IActionListProps { flow: 'claim' | 'request' @@ -314,6 +315,7 @@ export default function ActionList({ ) })} + setShowMinAmountError(false)} From c767d751dfea6b6bed6b39bfe4da7673878b3141 Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Fri, 31 Oct 2025 21:24:15 -0300 Subject: [PATCH 04/17] fix --- src/components/Common/ActionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Common/ActionList.tsx b/src/components/Common/ActionList.tsx index 4b419b754..bb891a05d 100644 --- a/src/components/Common/ActionList.tsx +++ b/src/components/Common/ActionList.tsx @@ -315,7 +315,7 @@ export default function ActionList({ ) })} - + {flow === 'claim' && !isLoggedIn && } setShowMinAmountError(false)} From fae636ee80cc7ddd09378be7f2586d1d947c5794 Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Sat, 1 Nov 2025 00:47:49 -0300 Subject: [PATCH 05/17] fix crisp --- src/app/(mobile-ui)/support/page.tsx | 11 ++++ src/components/CrispChat.tsx | 19 +++--- src/components/Global/SupportDrawer/index.tsx | 47 ++------------- src/hooks/useCrispIframeInitialization.ts | 58 +++++++++++++++++++ src/hooks/useCrispInitialization.ts | 45 ++++++++++++++ src/hooks/useCrispUserData.ts | 13 ++++- src/utils/crisp.ts | 22 +++++-- 7 files changed, 156 insertions(+), 59 deletions(-) create mode 100644 src/hooks/useCrispIframeInitialization.ts create mode 100644 src/hooks/useCrispInitialization.ts diff --git a/src/app/(mobile-ui)/support/page.tsx b/src/app/(mobile-ui)/support/page.tsx index d9ec886cf..b273b29ea 100644 --- a/src/app/(mobile-ui)/support/page.tsx +++ b/src/app/(mobile-ui)/support/page.tsx @@ -1,8 +1,19 @@ 'use client' +import { useCrispUserData } from '@/hooks/useCrispUserData' +import { useCrispIframeInitialization } from '@/hooks/useCrispIframeInitialization' +import { useRef } from 'react' + const SupportPage = () => { + const userData = useCrispUserData() + const iframeRef = useRef(null) + + // Initialize Crisp user data in iframe + useCrispIframeInitialization(iframeRef, userData, undefined, !!userData.userId) + return (