From 4f435cbe560390a697ec2520613f5bda693fc979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Wed, 9 Jul 2025 16:59:03 -0300 Subject: [PATCH 1/2] fix: small fixes around crosschain ui --- src/app/(mobile-ui)/withdraw/crypto/page.tsx | 1 + .../AddWithdraw/AddWithdrawRouterView.tsx | 8 +++- .../Global/RouteExpiryTimer/index.tsx | 40 +++++++++---------- src/components/Payment/PaymentForm/index.tsx | 1 + .../Payment/Views/Confirm.payment.view.tsx | 26 +++++++----- .../TransactionDetailsDrawer.tsx | 16 ++++---- .../Withdraw/views/Confirm.withdraw.view.tsx | 15 ++----- src/hooks/usePaymentInitiator.ts | 24 ++++++++++- 8 files changed, 78 insertions(+), 53 deletions(-) diff --git a/src/app/(mobile-ui)/withdraw/crypto/page.tsx b/src/app/(mobile-ui)/withdraw/crypto/page.tsx index e13dbb9a5..7ad846d07 100644 --- a/src/app/(mobile-ui)/withdraw/crypto/page.tsx +++ b/src/app/(mobile-ui)/withdraw/crypto/page.tsx @@ -115,6 +115,7 @@ export default function WithdrawCryptoPage() { clearErrors() dispatch(paymentActions.setChargeDetails(null)) + setIsPreparingReview(true) try { const completeWithdrawData = { ...data, amount: amountToWithdraw } diff --git a/src/components/AddWithdraw/AddWithdrawRouterView.tsx b/src/components/AddWithdraw/AddWithdrawRouterView.tsx index 50fc5b49c..e2b8ed69a 100644 --- a/src/components/AddWithdraw/AddWithdrawRouterView.tsx +++ b/src/components/AddWithdraw/AddWithdrawRouterView.tsx @@ -9,7 +9,13 @@ import { import EmptyState from '@/components/Global/EmptyStates/EmptyState' import NavHeader from '@/components/Global/NavHeader' import { SearchInput } from '@/components/SearchUsers/SearchInput' -import { RecentMethod, getUserPreferences, updateUserPreferences, shortenAddressLong, formatIban } from '@/utils/general.utils' +import { + RecentMethod, + getUserPreferences, + updateUserPreferences, + shortenAddressLong, + formatIban, +} from '@/utils/general.utils' import { useRouter } from 'next/navigation' import { FC, useEffect, useMemo, useState } from 'react' import { useUserStore } from '@/redux/hooks' diff --git a/src/components/Global/RouteExpiryTimer/index.tsx b/src/components/Global/RouteExpiryTimer/index.tsx index b0ea4f5b0..efb355277 100644 --- a/src/components/Global/RouteExpiryTimer/index.tsx +++ b/src/components/Global/RouteExpiryTimer/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react' +import React, { useState, useEffect, useCallback, useMemo } from 'react' import { twMerge } from 'tailwind-merge' interface RouteExpiryTimerProps { @@ -7,7 +7,7 @@ interface RouteExpiryTimerProps { onNearExpiry?: () => void // Called when timer gets close to expiry (e.g., 30 seconds) onExpired?: () => void // Called when timer expires className?: string - nearExpiryThresholdMs?: number // Default 30 seconds + nearExpiryThresholdPercentage?: number disableRefetch?: boolean // Disable refetching when user is signing transaction error?: string | null // Error message to display instead of timer } @@ -24,7 +24,7 @@ const RouteExpiryTimer: React.FC = ({ onNearExpiry, onExpired, className, - nearExpiryThresholdMs = 5000, // 5 seconds + nearExpiryThresholdPercentage = 0.1, // 10% of total duration disableRefetch = false, error = null, }) => { @@ -32,6 +32,9 @@ const RouteExpiryTimer: React.FC = ({ const [hasTriggeredNearExpiry, setHasTriggeredNearExpiry] = useState(false) const [hasExpired, setHasExpired] = useState(false) + const totalDurationMs = useMemo(() => (expiry ? parseInt(expiry) * 1000 - new Date().getTime() : 0), [expiry]) + const nearExpiryThresholdMs = useMemo(() => totalDurationMs * nearExpiryThresholdPercentage, [totalDurationMs]) + const calculateTimeRemaining = useCallback((): TimeRemaining | null => { if (!expiry) return null @@ -114,36 +117,29 @@ const RouteExpiryTimer: React.FC = ({ return `${paddedMinutes}:${paddedSeconds}` } - const getProgressPercentage = (): number => { - if (!timeRemaining || !expiry) return 0 - - // Assuming routes typically have 1-minute expiry (300 seconds) - // This could be made configurable if needed - const totalDurationMs = 1 * 60 * 1000 // 1 minutes + const progressPercentage = useMemo((): number => { + if (!timeRemaining || !totalDurationMs) return 0 const elapsedMs = totalDurationMs - timeRemaining.totalMs return Math.max(0, Math.min(100, (elapsedMs / totalDurationMs) * 100)) - } + }, [timeRemaining, totalDurationMs]) - const getProgressColor = (): string => { + const progressColor = useMemo((): string => { if (!timeRemaining) return 'bg-grey-3' - const percentage = getProgressPercentage() - // Green for first 70% - if (percentage < 70) return 'bg-green-500' + if (progressPercentage < 70) return 'bg-green-500' // Yellow for 70-85% - if (percentage < 85) return 'bg-yellow-500' + if (progressPercentage < 85) return 'bg-yellow-500' // Red for final 15% return 'bg-red' - } + }, [progressPercentage, timeRemaining]) - const shouldPulse = (): boolean => { + const shouldPulse = useMemo((): boolean => { if (isLoading) return true if (!timeRemaining) return false // Pulse when in red zone (85%+ progress) OR near expiry threshold - const progressPercentage = getProgressPercentage() return (progressPercentage >= 85 || timeRemaining.totalMs <= nearExpiryThresholdMs) && timeRemaining.totalMs > 0 - } + }, [progressPercentage, timeRemaining, isLoading, nearExpiryThresholdMs]) const getText = (): string => { if (error) return error @@ -177,11 +173,11 @@ const RouteExpiryTimer: React.FC = ({
diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index 0f0bda214..ea70f8064 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -633,6 +633,7 @@ export const PaymentForm = ({ disabled={!isAddMoneyFlow && (!!requestDetails?.tokenAmount || !!chargeDetails?.tokenAmount)} walletBalance={isActivePeanutWallet ? peanutWalletBalance : undefined} currency={currency} + hideBalance={isAddMoneyFlow} /> {/* diff --git a/src/components/Payment/Views/Confirm.payment.view.tsx b/src/components/Payment/Views/Confirm.payment.view.tsx index 563f5d030..d32224fc2 100644 --- a/src/components/Payment/Views/Confirm.payment.view.tsx +++ b/src/components/Payment/Views/Confirm.payment.view.tsx @@ -21,7 +21,7 @@ import { useWallet } from '@/hooks/wallet/useWallet' import { useAppDispatch, usePaymentStore, useWalletStore } from '@/redux/hooks' import { paymentActions } from '@/redux/slices/payment-slice' import { chargesApi } from '@/services/charges' -import { ErrorHandler, formatAmount, printableAddress, areEvmAddressesEqual } from '@/utils' +import { ErrorHandler, formatAmount, areEvmAddressesEqual } from '@/utils' import { useQueryClient } from '@tanstack/react-query' import { useSearchParams } from 'next/navigation' import { useCallback, useContext, useEffect, useMemo } from 'react' @@ -31,6 +31,7 @@ import { formatUnits } from 'viem' import type { Address } from 'viem' import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants' import { captureMessage } from '@sentry/nextjs' +import AddressLink from '@/components/Global/AddressLink' type ConfirmPaymentViewProps = { isPintaReq?: boolean @@ -106,8 +107,9 @@ export default function ConfirmPaymentView({ return ( <> - $ {estimatedGasCostUsd.toFixed(2)}{' '} - Sponsored by Peanut! + $ {estimatedGasCostUsd.toFixed(2)} + {' – '} + Sponsored by Peanut! ) }, [estimatedGasCostUsd, isFeeEstimationError, isUsingExternalWallet]) @@ -461,17 +463,23 @@ export default function ConfirmPaymentView({ /> )} - {isAddMoneyFlow && } + {isAddMoneyFlow && ( + + } + /> + )} diff --git a/src/components/TransactionDetails/TransactionDetailsDrawer.tsx b/src/components/TransactionDetails/TransactionDetailsDrawer.tsx index 4f23b1da8..57935d6ef 100644 --- a/src/components/TransactionDetails/TransactionDetailsDrawer.tsx +++ b/src/components/TransactionDetails/TransactionDetailsDrawer.tsx @@ -440,16 +440,16 @@ export const TransactionDetailsReceipt = ({ value={
- { - formatIban(transaction.extraDataForDrawer.depositInstructions - .iban) - } + {formatIban( + transaction.extraDataForDrawer.depositInstructions + .iban + )}
diff --git a/src/components/Withdraw/views/Confirm.withdraw.view.tsx b/src/components/Withdraw/views/Confirm.withdraw.view.tsx index 495f1bdbf..04a844141 100644 --- a/src/components/Withdraw/views/Confirm.withdraw.view.tsx +++ b/src/components/Withdraw/views/Confirm.withdraw.view.tsx @@ -67,8 +67,9 @@ export default function ConfirmWithdrawView({ if (networkFee < 0.01) return 'Sponsored by Peanut!' return ( <> - $ {networkFee.toFixed(2)}{' '} - Sponsored by Peanut! + $ {networkFee.toFixed(2)} + {' – '} + Sponsored by Peanut! ) }, [networkFee]) @@ -140,15 +141,7 @@ export default function ConfirmWithdrawView({ label="To" value={} /> - + diff --git a/src/hooks/usePaymentInitiator.ts b/src/hooks/usePaymentInitiator.ts index 51f9aba75..261ce49a8 100644 --- a/src/hooks/usePaymentInitiator.ts +++ b/src/hooks/usePaymentInitiator.ts @@ -33,6 +33,22 @@ import { getRoute, type PeanutCrossChainRoute } from '@/services/swap' import { estimateTransactionCostUsd } from '@/app/actions/tokens' import { captureException } from '@sentry/nextjs' +enum ELoadingStep { + IDLE = 'Idle', + PREPARING_TRANSACTION = 'Preparing Transaction', + SENDING_TRANSACTION = 'Sending Transaction', + CONFIRMING_TRANSACTION = 'Confirming Transaction', + UPDATING_PAYMENT_STATUS = 'Updating Payment Status', + CHARGE_CREATED = 'Charge Created', + ERROR = 'Error', + SUCCESS = 'Success', + FETCHING_CHARGE_DETAILS = 'Fetching Charge Details', + CREATING_CHARGE = 'Creating Charge', + SWITCHING_NETWORK = 'Switching Network', +} + +type LoadingStep = `${ELoadingStep}` + export interface InitiatePaymentPayload { recipient: ParsedURL['recipient'] tokenAmount: string @@ -84,7 +100,7 @@ export const usePaymentInitiator = () => { const [estimatedGasCostUsd, setEstimatedGasCostUsd] = useState(undefined) const [estimatedFromValue, setEstimatedFromValue] = useState('0') - const [loadingStep, setLoadingStep] = useState('Idle') + const [loadingStep, setLoadingStep] = useState('Idle') const [error, setError] = useState(null) const [createdChargeDetails, setCreatedChargeDetails] = useState(null) const [transactionHash, setTransactionHash] = useState(null) @@ -109,7 +125,11 @@ export const usePaymentInitiator = () => { }, [selectedTokenData, activeChargeDetails]) const isProcessing = useMemo( - () => loadingStep !== 'Idle' && loadingStep !== 'Success' && loadingStep !== 'Error', + () => + loadingStep !== 'Idle' && + loadingStep !== 'Success' && + loadingStep !== 'Error' && + loadingStep !== 'Charge Created', [loadingStep] ) From 559ada8b55960059ab56a5e7b4446a6f958783ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Wed, 9 Jul 2025 17:06:59 -0300 Subject: [PATCH 2/2] fix: avoid negative expiry time --- src/components/Global/RouteExpiryTimer/index.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/Global/RouteExpiryTimer/index.tsx b/src/components/Global/RouteExpiryTimer/index.tsx index efb355277..539d2d23a 100644 --- a/src/components/Global/RouteExpiryTimer/index.tsx +++ b/src/components/Global/RouteExpiryTimer/index.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react' import { twMerge } from 'tailwind-merge' interface RouteExpiryTimerProps { - expiry?: string // ISO string from route + expiry?: string // Unix timestamp in seconds isLoading?: boolean onNearExpiry?: () => void // Called when timer gets close to expiry (e.g., 30 seconds) onExpired?: () => void // Called when timer expires @@ -32,7 +32,12 @@ const RouteExpiryTimer: React.FC = ({ const [hasTriggeredNearExpiry, setHasTriggeredNearExpiry] = useState(false) const [hasExpired, setHasExpired] = useState(false) - const totalDurationMs = useMemo(() => (expiry ? parseInt(expiry) * 1000 - new Date().getTime() : 0), [expiry]) + const totalDurationMs = useMemo(() => { + if (!expiry) return 0 + const expiryMs = parseInt(expiry, 10) * 1000 + const diff = expiryMs - Date.now() + return Math.max(0, diff) + }, [expiry]) const nearExpiryThresholdMs = useMemo(() => totalDurationMs * nearExpiryThresholdPercentage, [totalDurationMs]) const calculateTimeRemaining = useCallback((): TimeRemaining | null => {