Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/components/Common/ActionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
18 changes: 16 additions & 2 deletions src/components/Global/Contributors/ContributorCard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
'use client'
import { type Payment } from '@/services/services.types'
import Card, { type CardPosition } from '../Card'
import AvatarWithBadge from '@/components/Profile/AvatarWithBadge'
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
Expand All @@ -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 (
<Card position={position} className="cursor-pointer">
<Card position={position}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div
onClick={() => {
if (contributor.isPeanutUser) {
router.push(`/${contributor.username}`)
}
}}
className={twMerge('flex items-center gap-2', contributor.isPeanutUser && 'cursor-pointer')}
>
<AvatarWithBadge
name={contributor.username ?? ''}
size={'extra-small'}
Expand Down
37 changes: 33 additions & 4 deletions src/components/Global/ProgressBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { COIN_ICON } from '@/assets'
import Image from 'next/image'
import React from 'react'
import { twMerge } from 'tailwind-merge'
import { formatExtendedNumber } from '@/utils/general.utils'

interface ProgressBarProps {
goal: number
Expand All @@ -12,14 +13,42 @@ interface ProgressBarProps {
const ProgressBar: React.FC<ProgressBarProps> = ({ 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!'
Expand Down
6 changes: 5 additions & 1 deletion src/components/Global/TokenAmountInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
50 changes: 26 additions & 24 deletions src/components/Payment/PaymentForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(() => {
Expand Down Expand Up @@ -301,17 +306,27 @@ 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

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<boolean>(() => {
let amountIsSet = false
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1459,7 +1459,7 @@ export const TransactionDetailsReceipt = ({
{requestPotContributors.length > 0 && (
<>
<h2 className="text-base font-bold text-black">Contributors ({requestPotContributors.length})</h2>
<div className="max-h-36 overflow-y-auto">
<div className="overflow-y-auto">
{requestPotContributors.map((contributor, index) => (
<ContributorCard
position={getCardPosition(index, requestPotContributors.length)}
Expand Down
7 changes: 5 additions & 2 deletions src/utils/general.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@ export const getHeaderTitle = (pathname: string) => {
* @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'

Expand All @@ -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)
}

Expand Down Expand Up @@ -902,13 +902,16 @@ export const getContributorsFromCharge = (charges: ChargeEntry[]) => {
username = successfulPayment.payerAccount.identifier
}

const isPeanutUser = successfulPayment?.payerAccount?.type === AccountType.PEANUT_WALLET

return {
uuid: charge.uuid,
payments: charge.payments,
amount: charge.tokenAmount,
username,
fulfillmentPayment: charge.fulfillmentPayment,
isUserVerified: successfulPayment?.payerAccount?.user?.bridgeKycStatus === 'approved',
isPeanutUser,
}
})
}
Loading