diff --git a/src/components/Common/ActionList.tsx b/src/components/Common/ActionList.tsx index 331a81969..6ec704230 100644 --- a/src/components/Common/ActionList.tsx +++ b/src/components/Common/ActionList.tsx @@ -188,8 +188,14 @@ export default function ActionList({ break } } else if (flow === 'request' && requestLinkData) { - if (method.id === 'bank' && amountInUsd < 1) { - setShowMinAmountError(true) + // @dev TODO: Fix req fulfillment with bank properly post devconnect + if (method.id === 'bank') { + if (user?.user) { + router.push('/add-money') + } else { + const redirectUri = encodeURIComponent('/add-money') + router.push(`/setup?redirect_uri=${redirectUri}`) + } return } diff --git a/src/components/Global/Contributors/ContributorCard.tsx b/src/components/Global/Contributors/ContributorCard.tsx index d351b9e9f..e31207355 100644 --- a/src/components/Global/Contributors/ContributorCard.tsx +++ b/src/components/Global/Contributors/ContributorCard.tsx @@ -1,3 +1,4 @@ +'use client' import { type Payment } from '@/services/services.types' import Card, { type CardPosition } from '../Card' import AvatarWithBadge from '@/components/Profile/AvatarWithBadge' @@ -5,6 +6,8 @@ import { getColorForUsername } from '@/utils/color.utils' import { VerifiedUserLabel } from '@/components/UserHeader' import { formatTokenAmount } from '@/utils' import { isAddress } from 'viem' +import { useRouter } from 'next/navigation' +import { twMerge } from 'tailwind-merge' export type Contributor = { uuid: string @@ -13,15 +16,26 @@ export type Contributor = { username: string | undefined fulfillmentPayment: Payment | null isUserVerified: boolean + isPeanutUser: boolean } const ContributorCard = ({ contributor, position }: { contributor: Contributor; position: CardPosition }) => { const colors = getColorForUsername(contributor.username ?? '') const isEvmAddress = isAddress(contributor.username ?? '') + + const router = useRouter() + return ( - +
-
+
{ + if (contributor.isPeanutUser) { + router.push(`/${contributor.username}`) + } + }} + className={twMerge('flex items-center gap-2', contributor.isPeanutUser && 'cursor-pointer')} + > = ({ goal, progress, isClosed }) => { const isOverGoal = progress > goal && goal > 0 const isGoalAchieved = progress >= goal && !isOverGoal && goal > 0 - const totalValue = isOverGoal ? progress : goal + + // Calculate actual ratio and enforce minimum visual distance when over goal + const MIN_VISUAL_DISTANCE = 30 // 30% minimum distance between markers + let totalValue = isOverGoal ? progress : goal + let visualGoalPercentage = 0 + let visualProgressPercentage = 0 + + if (isOverGoal && goal > 0) { + const actualRatio = (progress / goal) * 100 + if (actualRatio < 100 + MIN_VISUAL_DISTANCE) { + // Progress is too close to goal, enforce minimum distance + // Map goal to ~90% and progress to 100% + visualGoalPercentage = 100 - MIN_VISUAL_DISTANCE + visualProgressPercentage = 100 + } else { + // Progress is far enough, show actual ratio + totalValue = progress + visualGoalPercentage = (goal / totalValue) * 100 + visualProgressPercentage = 100 + } + } // Guard against division by zero and clamp percentages to valid ranges - const goalPercentage = totalValue > 0 ? Math.min(Math.max((goal / totalValue) * 100, 0), 100) : 0 - const progressPercentage = totalValue > 0 ? Math.min(Math.max((progress / totalValue) * 100, 0), 100) : 0 + const goalPercentage = isOverGoal + ? visualGoalPercentage + : totalValue > 0 + ? Math.min(Math.max((goal / totalValue) * 100, 0), 100) + : 0 + const progressPercentage = isOverGoal + ? visualProgressPercentage + : totalValue > 0 + ? Math.min(Math.max((progress / totalValue) * 100, 0), 100) + : 0 const percentage = goal > 0 ? Math.min(Math.max(Math.round((progress / goal) * 100), 0), 100) : 0 - const formatCurrency = (value: number) => `$${value.toFixed(2)}` + const formatCurrency = (value: number) => `$${formatExtendedNumber(value, 4)}` const getStatusText = () => { if (isOverGoal) return 'Goal exceeded!' diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx index f077b3dc6..003c3404a 100644 --- a/src/components/Global/TokenAmountInput/index.tsx +++ b/src/components/Global/TokenAmountInput/index.tsx @@ -154,7 +154,11 @@ const TokenAmountInput = ({ if (maxAmount) { const selectedPercentage = value[0] const selectedAmount = parseFloat(((selectedPercentage / 100) * maxAmount).toFixed(4)).toString() - onChange(selectedAmount, isInputUsd) + const maxDecimals = displayMode === 'FIAT' || displayMode === 'STABLE' || isInputUsd ? 2 : decimals + const formattedAmount = formatTokenAmount(selectedAmount, maxDecimals, true) + if (formattedAmount) { + onChange(formattedAmount, isInputUsd) + } } }, [maxAmount, onChange] diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index e9ff34ba2..e5b02e81e 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -168,8 +168,13 @@ export const PaymentForm = ({ const isActivePeanutWallet = useMemo(() => !!user && isPeanutWalletConnected, [user, isPeanutWalletConnected]) + const isRequestPotLink = !!chargeDetails?.requestLink + useEffect(() => { - if (initialSetupDone || showRequestPotInitialView) return + // skip this step for request pot payments + // Amount is set by the user so we dont need to manually update it + // chain and token are also USDC arb always, for cross-chain we use Daimo + if (initialSetupDone || showRequestPotInitialView || isRequestPotLink) return if (amount) { setInputTokenAmount(amount) @@ -192,7 +197,7 @@ export const PaymentForm = ({ } setInitialSetupDone(true) - }, [chain, token, amount, initialSetupDone, requestDetails, showRequestPotInitialView]) + }, [chain, token, amount, initialSetupDone, requestDetails, showRequestPotInitialView, isRequestPotLink]) // reset error when component mounts or recipient changes useEffect(() => { @@ -301,7 +306,16 @@ export const PaymentForm = ({ // Calculate USD value when requested token price is available useEffect(() => { - if (showRequestPotInitialView || !requestedTokenPriceData?.price || !requestDetails?.tokenAmount) return + // skip this step for request pot payments + // Amount is set by the user so we dont need to manually update it + // No usd conversion needed because amount will always be USDC + if ( + showRequestPotInitialView || + !requestedTokenPriceData?.price || + !requestDetails?.tokenAmount || + isRequestPotLink + ) + return const tokenAmount = parseFloat(requestDetails.tokenAmount) if (isNaN(tokenAmount) || tokenAmount <= 0) return @@ -309,9 +323,10 @@ export const PaymentForm = ({ if (isNaN(requestedTokenPriceData.price) || requestedTokenPriceData.price === 0) return const usdValue = formatAmount(tokenAmount * requestedTokenPriceData.price) + setInputTokenAmount(usdValue) setUsdValue(usdValue) - }, [requestedTokenPriceData?.price, requestDetails?.tokenAmount, showRequestPotInitialView]) + }, [requestedTokenPriceData?.price, requestDetails?.tokenAmount, showRequestPotInitialView, isRequestPotLink]) const canInitiatePayment = useMemo(() => { let amountIsSet = false @@ -581,10 +596,12 @@ export const PaymentForm = ({ // Initialize inputTokenAmount useEffect(() => { - if (amount && !inputTokenAmount && !initialSetupDone) { + // skip this step for request pot payments + // Amount is set by the user so we dont need to manually update it + if (amount && !inputTokenAmount && !initialSetupDone && !showRequestPotInitialView) { setInputTokenAmount(amount) } - }, [amount, inputTokenAmount, initialSetupDone]) + }, [amount, inputTokenAmount, initialSetupDone, showRequestPotInitialView]) useEffect(() => { const stepFromURL = searchParams.get('step') @@ -620,6 +637,7 @@ export const PaymentForm = ({ // ensure inputTokenAmount is a valid positive number before allowing payment const numericAmount = parseFloat(inputTokenAmount) + if (isNaN(numericAmount) || numericAmount <= 0) { if (!isExternalWalletFlow) return true } @@ -691,24 +709,8 @@ export const PaymentForm = ({ if (contributionAmounts.length === 0) return { percentage: 0, suggestedAmount: 0 } - const avgContribution = contributionAmounts.reduce((sum, amt) => sum + amt, 0) / contributionAmounts.length - - // Calculate remaining amount (could be negative if over-contributed) - const remaining = totalAmount - totalCollected - let suggestedAmount: number - - // If pot is already full or over-filled, suggest minimum contribution - if (remaining <= 0) { - // Pot is full/overfilled - suggest the smallest previous contribution or 10% of pot - const minContribution = Math.min(...contributionAmounts) - suggestedAmount = Math.min(minContribution, totalAmount * 0.1) - } else if (remaining < avgContribution) { - // If remaining is less than average, suggest the remaining amount - suggestedAmount = remaining - } else { - // Otherwise, suggest the average contribution (most common pattern) - suggestedAmount = avgContribution - } + // suggest the average contribution (most common pattern) + const suggestedAmount = contributionAmounts.reduce((sum, amt) => sum + amt, 0) / contributionAmounts.length // Convert amount to percentage of total pot const percentage = (suggestedAmount / totalAmount) * 100 diff --git a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx index 304c1542b..b144a1de9 100644 --- a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx +++ b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx @@ -1459,7 +1459,7 @@ export const TransactionDetailsReceipt = ({ {requestPotContributors.length > 0 && ( <>

Contributors ({requestPotContributors.length})

-
+
{requestPotContributors.map((contributor, index) => ( { * @param amount - The number or string to format. * @returns A formatted string with appropriate suffix. */ -export const formatExtendedNumber = (amount: string | number): string => { +export const formatExtendedNumber = (amount: string | number, minDigitsForFomatting: number = 6): string => { // Handle null/undefined/invalid inputs if (!amount && amount !== 0) return '0' @@ -669,7 +669,7 @@ export const formatExtendedNumber = (amount: string | number): string => { const totalDigits = amount.toString().replace(/[.-]/g, '').length // If 6 or fewer digits, just use formatAmount - if (totalDigits <= 6) { + if (totalDigits <= minDigitsForFomatting) { return formatAmount(num) } @@ -902,6 +902,8 @@ export const getContributorsFromCharge = (charges: ChargeEntry[]) => { username = successfulPayment.payerAccount.identifier } + const isPeanutUser = successfulPayment?.payerAccount?.type === AccountType.PEANUT_WALLET + return { uuid: charge.uuid, payments: charge.payments, @@ -909,6 +911,7 @@ export const getContributorsFromCharge = (charges: ChargeEntry[]) => { username, fulfillmentPayment: charge.fulfillmentPayment, isUserVerified: successfulPayment?.payerAccount?.user?.bridgeKycStatus === 'approved', + isPeanutUser, } }) }