diff --git a/src/app/(mobile-ui)/home/page.tsx b/src/app/(mobile-ui)/home/page.tsx index d28b43dbb..b0ed3aa51 100644 --- a/src/app/(mobile-ui)/home/page.tsx +++ b/src/app/(mobile-ui)/home/page.tsx @@ -24,7 +24,6 @@ import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' import { PostSignupActionManager } from '@/components/Global/PostSignupActionManager' import { useWithdrawFlow } from '@/context/WithdrawFlowContext' import { useClaimBankFlow } from '@/context/ClaimBankFlowContext' -import { useDeviceType } from '@/hooks/useGetDeviceType' import { useNotifications } from '@/hooks/useNotifications' import useKycStatus from '@/hooks/useKycStatus' import { useCardPioneerInfo } from '@/hooks/useCardPioneerInfo' @@ -57,7 +56,6 @@ export default function Home() { const { balance, isFetchingBalance } = useWallet() const { resetFlow: resetClaimBankFlow } = useClaimBankFlow() const { resetWithdrawFlow } = useWithdrawFlow() - const { deviceType } = useDeviceType() const { user } = useUserStore() const [isBalanceHidden, setIsBalanceHidden] = useState(() => { const prefs = user ? getUserPreferences(user.user.userId) : undefined diff --git a/src/app/(mobile-ui)/points/page.tsx b/src/app/(mobile-ui)/points/page.tsx index 11b549d56..369a43d0b 100644 --- a/src/app/(mobile-ui)/points/page.tsx +++ b/src/app/(mobile-ui)/points/page.tsx @@ -126,7 +126,7 @@ const PointsPage = () => { return ( <> {number} - {suffix && {suffix}} + {suffix && {suffix}} ) })()}{' '} diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index 4282c07ca..1cd208fbc 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -1057,25 +1057,25 @@ export default function QRPayPage() { ) } - // Show maintenance error if provider is disabled - if (isProviderDisabled) { - // Get user-facing payment method name - const paymentMethodName = useMemo(() => { - if (paymentProcessor === 'MANTECA') { - switch (qrType) { - case EQrType.PIX: - return 'PIX' - case EQrType.MERCADO_PAGO: - return 'Mercado Pago' - case EQrType.ARGENTINA_QR3: - return 'QR' - default: - return 'QR' - } + // get user-facing payment method name for maintenance screen + const paymentMethodName = useMemo(() => { + if (paymentProcessor === 'MANTECA') { + switch (qrType) { + case EQrType.PIX: + return 'PIX' + case EQrType.MERCADO_PAGO: + return 'Mercado Pago' + case EQrType.ARGENTINA_QR3: + return 'QR' + default: + return 'QR' } - return 'SimpleFi' - }, []) + } + return 'SimpleFi' + }, [paymentProcessor, qrType]) + // Show maintenance error if provider is disabled + if (isProviderDisabled) { return (
@@ -1267,9 +1267,9 @@ export default function QRPayPage() { {/* Perk Success Banner - Show after claiming */} {(perkClaimed || qrPayment?.perk?.claimed) && ( - -
- star + +
+ star

Peanut got you!

@@ -1332,7 +1332,7 @@ export default function QRPayPage() { WebkitTapHighlightColor: 'transparent', }} > - {/* Black progress fill from left to right */} + {/* progress fill from left to right */}
- Claim Peanut Perk Now! + {(() => { + const label = 'Claim Peanut Perk Now!' + return ( + <> + {label} + + {label} + + + ) + })()} ) : ( <> diff --git a/src/app/(mobile-ui)/withdraw/manteca/page.tsx b/src/app/(mobile-ui)/withdraw/manteca/page.tsx index 158ee8142..864952051 100644 --- a/src/app/(mobile-ui)/withdraw/manteca/page.tsx +++ b/src/app/(mobile-ui)/withdraw/manteca/page.tsx @@ -12,7 +12,6 @@ import { Icon } from '@/components/Global/Icons/Icon' import PeanutLoading from '@/components/Global/PeanutLoading' import { mantecaApi, type WithdrawPriceLock } from '@/services/manteca' import { useCurrency } from '@/hooks/useCurrency' -import { isTxReverted } from '@/utils/general.utils' import { loadingStateContext } from '@/context' import { countryData } from '@/components/AddMoney/consts' import Image from 'next/image' @@ -27,7 +26,6 @@ import { import ValidatedInput from '@/components/Global/ValidatedInput' import AmountInput from '@/components/Global/AmountInput' import { formatUnits, parseUnits } from 'viem' -import type { TransactionReceipt, Hash } from 'viem' import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow' import { useAuth } from '@/context/authContext' import { useModalsContext } from '@/context/ModalsContext' @@ -623,7 +621,7 @@ export default function MantecaWithdrawFlow() {

- You're withdrawing + You're sending

{currencyCode} {formatNumberForDisplay(currencyAmount, { maxDecimals: 2 })} @@ -737,7 +735,7 @@ export default function MantecaWithdrawFlow() {

- You're withdrawing + You're sending

{currencyCode}{' '} diff --git a/src/components/Global/Badges/StatusBadge.tsx b/src/components/Global/Badges/StatusBadge.tsx index 1f37ab48a..6ac1d2e3d 100644 --- a/src/components/Global/Badges/StatusBadge.tsx +++ b/src/components/Global/Badges/StatusBadge.tsx @@ -32,7 +32,7 @@ const StatusBadge: React.FC = ({ status, className, size = 'sm case 'cancelled': return 'bg-error-1 text-error border border-error-2' case 'refunded': - return 'bg-secondary-4 text-yellow-6 border border-yellow-7' + return 'bg-success-2 text-success-4 border border-success-5' case 'soon': case 'custom': return 'bg-primary-3 text-primary-4' diff --git a/src/components/Global/ExchangeRateWidget/index.tsx b/src/components/Global/ExchangeRateWidget/index.tsx index 39ccc4d3e..98be84868 100644 --- a/src/components/Global/ExchangeRateWidget/index.tsx +++ b/src/components/Global/ExchangeRateWidget/index.tsx @@ -4,7 +4,7 @@ import { useDebounce } from '@/hooks/useDebounce' import { useExchangeRate } from '@/hooks/useExchangeRate' import Image from 'next/image' import { useRouter, useSearchParams } from 'next/navigation' -import { type FC, useCallback, useEffect, useMemo } from 'react' +import { type FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Icon, type IconName } from '../Icons/Icon' import { Button } from '@/components/0_Bruddle/Button' @@ -90,6 +90,28 @@ const ExchangeRateWidget: FC = ({ ctaLabel, ctaIcon, c [updateUrlParams, sourceCurrency] ) + const [isSwapping, setIsSwapping] = useState(false) + const skipNextDebounceSyncRef = useRef(false) + + const swapCurrencies = useCallback(() => { + setIsSwapping(true) + skipNextDebounceSyncRef.current = true + const newAmount = + typeof destinationAmount === 'number' && destinationAmount > 0 + ? Math.round(destinationAmount * 100) / 100 + : undefined + updateUrlParams({ from: destinationCurrency, to: sourceCurrency, amount: newAmount }) + }, [sourceCurrency, destinationCurrency, destinationAmount, updateUrlParams]) + + // clear swapping state once exchange rate hook finishes recalculating + useEffect(() => { + if (isSwapping && !isLoading) { + setIsSwapping(false) + } + }, [isSwapping, isLoading]) + + const showLoading = isLoading || isSwapping + // Enforce USD rule: at least one currency must be USD useEffect(() => { if (sourceCurrency !== 'USD' && destinationCurrency !== 'USD') { @@ -100,6 +122,10 @@ const ExchangeRateWidget: FC = ({ ctaLabel, ctaIcon, c // Update URL when source amount changes (only for valid numbers) useEffect(() => { + if (skipNextDebounceSyncRef.current) { + skipNextDebounceSyncRef.current = false + return + } if (typeof debouncedSourceAmount === 'number' && debouncedSourceAmount !== urlSourceAmount) { updateUrlParams({ amount: debouncedSourceAmount }) } @@ -125,7 +151,7 @@ const ExchangeRateWidget: FC = ({ ctaLabel, ctaIcon, c

You Send

- {isLoading ? ( + {showLoading ? (
@@ -167,10 +193,18 @@ const ExchangeRateWidget: FC = ({ ctaLabel, ctaIcon, c
+ +

Recipient Gets

- {isLoading ? ( + {showLoading ? (
@@ -212,7 +246,7 @@ const ExchangeRateWidget: FC = ({ ctaLabel, ctaIcon, c
- {isLoading ? ( + {showLoading ? (
) : isError ? ( Rate currently unavailable diff --git a/src/components/Global/Icons/Icon.tsx b/src/components/Global/Icons/Icon.tsx index bcd77f4d8..8e2414415 100644 --- a/src/components/Global/Icons/Icon.tsx +++ b/src/components/Global/Icons/Icon.tsx @@ -56,7 +56,6 @@ import { VerifiedUserOutlined, EmojiEventsOutlined, LockOutlined, - CallSplitRounded, GroupsRounded, VpnLockOutlined, CameraswitchRounded, diff --git a/src/components/Global/PeanutActionDetailsCard/index.tsx b/src/components/Global/PeanutActionDetailsCard/index.tsx index 0054d0661..cd37c9160 100644 --- a/src/components/Global/PeanutActionDetailsCard/index.tsx +++ b/src/components/Global/PeanutActionDetailsCard/index.tsx @@ -104,7 +104,7 @@ export default function PeanutActionDetailsCard({ else title = `${renderRecipient()} sent you` } if (transactionType === 'ADD_MONEY' || transactionType === 'ADD_MONEY_BANK_ACCOUNT') title = `You're adding` - if (transactionType === 'WITHDRAW' || transactionType === 'WITHDRAW_BANK_ACCOUNT') title = `You're withdrawing` + if (transactionType === 'WITHDRAW' || transactionType === 'WITHDRAW_BANK_ACCOUNT') title = `You're sending` if (transactionType === 'CLAIM_LINK_BANK_ACCOUNT') { if (viewType === 'SUCCESS') { title = 'You will receive' diff --git a/src/components/Global/StatusPill/index.tsx b/src/components/Global/StatusPill/index.tsx index 4f7f45a40..9f400c20c 100644 --- a/src/components/Global/StatusPill/index.tsx +++ b/src/components/Global/StatusPill/index.tsx @@ -13,7 +13,7 @@ const StatusPill = ({ status }: StatusPillProps) => { completed: 'border-success-5 bg-success-2 text-success-4', pending: 'border-yellow-8 bg-secondary-4 text-yellow-6', cancelled: 'border-error-2 bg-error-1 text-error', - refunded: 'border-yellow-8 bg-secondary-4 text-yellow-6', + refunded: 'border-success-5 bg-success-2 text-success-4', failed: 'border-error-2 bg-error-1 text-error', processing: 'border-yellow-8 bg-secondary-4 text-yellow-6', soon: 'border-yellow-8 bg-secondary-4 text-yellow-6', diff --git a/src/components/Global/SupportDrawer/index.tsx b/src/components/Global/SupportDrawer/index.tsx index 74e2eb5a7..a6d311772 100644 --- a/src/components/Global/SupportDrawer/index.tsx +++ b/src/components/Global/SupportDrawer/index.tsx @@ -1,26 +1,52 @@ 'use client' -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef, useCallback } from 'react' import { useModalsContext } from '@/context/ModalsContext' import { useCrispUserData } from '@/hooks/useCrispUserData' import { useCrispProxyUrl } from '@/hooks/useCrispProxyUrl' -import { Drawer, DrawerContent, DrawerTitle } from '../Drawer' import PeanutLoading from '../PeanutLoading' +const DISMISS_THRESHOLD = 100 + const SupportDrawer = () => { const { isSupportModalOpen, setIsSupportModalOpen, supportPrefilledMessage: prefilledMessage } = useModalsContext() const userData = useCrispUserData() - const [isLoading, setIsLoading] = useState(true) + const [isCrispReady, setIsCrispReady] = useState(false) const crispProxyUrl = useCrispProxyUrl(userData, prefilledMessage) + // drag-to-dismiss state + const panelRef = useRef(null) + const dragStartY = useRef(null) + const [dragOffset, setDragOffset] = useState(0) + const isDragging = dragStartY.current !== null + + const handleTouchStart = useCallback((e: React.TouchEvent) => { + dragStartY.current = e.touches[0].clientY + }, []) + + const handleTouchMove = useCallback((e: React.TouchEvent) => { + if (dragStartY.current === null) return + const delta = e.touches[0].clientY - dragStartY.current + // only allow dragging downward + setDragOffset(Math.max(0, delta)) + }, []) + + const handleTouchEnd = useCallback(() => { + if (dragOffset > DISMISS_THRESHOLD) { + setIsSupportModalOpen(false) + } + dragStartY.current = null + setDragOffset(0) + }, [dragOffset, setIsSupportModalOpen]) + + // listen for crisp ready once — persists across open/close cycles 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) + setIsCrispReady(true) } } @@ -28,33 +54,70 @@ const SupportDrawer = () => { return () => window.removeEventListener('message', handleMessage) }, []) - // Reset loading state when drawer closes + // close on escape useEffect(() => { - if (!isSupportModalOpen) { - setIsLoading(true) + if (!isSupportModalOpen) return + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') setIsSupportModalOpen(false) } - }, [isSupportModalOpen]) + window.addEventListener('keydown', handleEscape) + return () => window.removeEventListener('keydown', handleEscape) + }, [isSupportModalOpen, setIsSupportModalOpen]) return ( - - - Support -
- {isLoading && ( -
- -
- )} -