+
{/* Container with black border and rounded corners */}
) {
+ // Use internal state for the slider value to enable magnetic snapping
+ const [internalValue, setInternalValue] = React.useState
(controlledValue || defaultValue)
+
+ // Sync with controlled value if it changes externally
+ React.useEffect(() => {
+ if (controlledValue) {
+ setInternalValue(controlledValue)
+ }
+ }, [controlledValue])
+
+ // Check if current value is at a snap point (exact match)
+ const activeSnapPoint = React.useMemo(() => {
+ return SNAP_POINTS.find((snapPoint) => Math.abs(internalValue[0] - snapPoint) < 0.5)
+ }, [internalValue])
+
+ // Soft snap to nearby snap points with ±5% threshold
+ const handleValueChange = React.useCallback(
+ (newValue: number[]) => {
+ const rawValue = newValue[0]
+ let finalValue = rawValue
+
+ // Check if we're within snap threshold of any snap point
+ for (const snapPoint of SNAP_POINTS) {
+ if (Math.abs(rawValue - snapPoint) <= SNAP_THRESHOLD) {
+ finalValue = snapPoint
+ break
+ }
+ }
+
+ const finalArray = [finalValue]
+
+ // Only update if the value actually changed
+ if (internalValue[0] !== finalValue) {
+ setInternalValue(finalArray)
+ onValueChange?.(finalArray)
+ }
+ },
+ [onValueChange, internalValue]
+ )
+
+ return (
+
+
+
+
+
+
+
+
+ {/* Vertical tick mark - only visible when at a snap point */}
+ {activeSnapPoint !== undefined && (
+
+ )}
+
+ {/* White circle with border on top of the tick */}
+
+
+ {/* Current value label */}
+
+ {internalValue[0] % 1 === 0 ? internalValue[0].toFixed(0) : internalValue[0].toFixed(2)}%
+
+
+
+
+ )
+}
+
+export { Slider }
diff --git a/src/components/Global/StatusPill/index.tsx b/src/components/Global/StatusPill/index.tsx
index 1daec7db8..89a986a07 100644
--- a/src/components/Global/StatusPill/index.tsx
+++ b/src/components/Global/StatusPill/index.tsx
@@ -16,6 +16,7 @@ const StatusPill = ({ status }: StatusPillProps) => {
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',
+ closed: 'border-success-5 bg-success-2 text-success-4',
}
const iconClasses: Record = {
@@ -25,21 +26,23 @@ const StatusPill = ({ status }: StatusPillProps) => {
soon: 'pending',
pending: 'pending',
cancelled: 'cancel',
+ closed: 'success',
}
const iconSize: Record = {
- completed: 10,
- failed: 7,
+ completed: 7,
+ failed: 6,
processing: 10,
- soon: 10,
- pending: 10,
- cancelled: 7,
+ soon: 7,
+ pending: 8,
+ cancelled: 6,
+ closed: 7,
}
return (
diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx
index 25f011078..68fb25d7d 100644
--- a/src/components/Global/TokenAmountInput/index.tsx
+++ b/src/components/Global/TokenAmountInput/index.tsx
@@ -4,6 +4,8 @@ import { formatAmountWithoutComma, formatTokenAmount, formatCurrency } from '@/u
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import Icon from '../Icon'
import { twMerge } from 'tailwind-merge'
+import { Icon as IconComponent } from '@/components/Global/Icons/Icon'
+import { Slider } from '../Slider'
interface TokenAmountInputProps {
className?: string
@@ -22,6 +24,10 @@ interface TokenAmountInputProps {
}
hideCurrencyToggle?: boolean
hideBalance?: boolean
+ showInfoText?: boolean
+ infoText?: string
+ showSlider?: boolean
+ maxAmount?: number
isInitialInputUsd?: boolean
}
@@ -38,6 +44,10 @@ const TokenAmountInput = ({
setUsdValue,
hideCurrencyToggle = false,
hideBalance = false,
+ infoText,
+ showInfoText,
+ showSlider = false,
+ maxAmount,
isInitialInputUsd = false,
}: TokenAmountInputProps) => {
const { selectedTokenData } = useContext(tokenSelectorContext)
@@ -128,6 +138,17 @@ const TokenAmountInput = ({
[displayMode, currency?.price, selectedTokenData?.price, calculateAlternativeValue]
)
+ const onSliderValueChange = useCallback(
+ (value: number[]) => {
+ if (maxAmount) {
+ const selectedPercentage = value[0]
+ const selectedAmount = parseFloat(((selectedPercentage / 100) * maxAmount).toFixed(4)).toString()
+ onChange(selectedAmount, isInputUsd)
+ }
+ },
+ [maxAmount, onChange]
+ )
+
const showConversion = useMemo(() => {
return !hideCurrencyToggle && (displayMode === 'TOKEN' || displayMode === 'FIAT')
}, [hideCurrencyToggle, displayMode])
@@ -194,6 +215,13 @@ const TokenAmountInput = ({
const formRef = useRef(null)
+ const sliderValue = useMemo(() => {
+ if (!maxAmount || !tokenValue) return [0]
+ const tokenNum = parseFloat(tokenValue.replace(/,/g, ''))
+ const usdValue = tokenNum * (selectedTokenData?.price ?? 1)
+ return [(usdValue / maxAmount) * 100]
+ }, [maxAmount, tokenValue, selectedTokenData?.price])
+
const handleContainerClick = () => {
if (inputRef.current) {
inputRef.current.focus()
@@ -203,7 +231,7 @@ const TokenAmountInput = ({
return (
)}
-
{/* Conversion toggle */}
{showConversion && (
)}
+ {showInfoText && infoText && (
+
+ )}
+ {showSlider && maxAmount && (
+
+
+
+ )}
)
}
diff --git a/src/components/Kyc/KycStatusItem.tsx b/src/components/Kyc/KycStatusItem.tsx
index 0768af89d..9d7181d2e 100644
--- a/src/components/Kyc/KycStatusItem.tsx
+++ b/src/components/Kyc/KycStatusItem.tsx
@@ -99,7 +99,7 @@ export const KycStatusItem = ({
}
export const KYCStatusIcon = () => {
- return
+ return
}
export const KYCStatusDrawerItem = ({ status }: { status: StatusType }) => {
diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx
index 61e87a50f..48e62ac1c 100644
--- a/src/components/Payment/PaymentForm/index.tsx
+++ b/src/components/Payment/PaymentForm/index.tsx
@@ -22,7 +22,7 @@ import { type ParsedURL } from '@/lib/url-parser/types/payment'
import { useAppDispatch, usePaymentStore } from '@/redux/hooks'
import { paymentActions } from '@/redux/slices/payment-slice'
import { walletActions } from '@/redux/slices/wallet-slice'
-import { areEvmAddressesEqual, ErrorHandler, formatAmount, formatCurrency } from '@/utils'
+import { areEvmAddressesEqual, ErrorHandler, formatAmount, formatCurrency, getContributorsFromCharge } from '@/utils'
import { useAppKit, useDisconnect } from '@reown/appkit/react'
import Image from 'next/image'
import { useRouter, useSearchParams } from 'next/navigation'
@@ -35,6 +35,8 @@ import { type PaymentFlow } from '@/app/[...recipient]/client'
import MantecaFulfillment from '../Views/MantecaFulfillment.view'
import { invitesApi } from '@/services/invites'
import { EInviteType } from '@/services/services.types'
+import ContributorCard from '@/components/Global/Contributors/ContributorCard'
+import { getCardPosition } from '@/components/Global/Card'
export type PaymentFlowProps = {
isExternalWalletFlow?: boolean
@@ -49,6 +51,7 @@ export type PaymentFlowProps = {
setCurrencyAmount?: (currencyvalue: string | undefined) => void
headerTitle?: string
flow?: PaymentFlow
+ showRequestPotInitialView?: boolean
}
export type PaymentFormProps = ParsedURL & PaymentFlowProps
@@ -65,6 +68,7 @@ export const PaymentForm = ({
isDirectUsdPayment,
headerTitle,
flow,
+ showRequestPotInitialView,
}: PaymentFormProps) => {
const dispatch = useAppDispatch()
const router = useRouter()
@@ -222,7 +226,11 @@ export const PaymentForm = ({
}
} else {
// regular send/pay
- if (isActivePeanutWallet && areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN)) {
+ if (
+ !showRequestPotInitialView && // don't apply balance check on request pot payment initial view
+ isActivePeanutWallet &&
+ areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN)
+ ) {
// peanut wallet payment
const walletNumeric = parseFloat(String(peanutWalletBalance).replace(/,/g, ''))
if (walletNumeric < parsedInputAmount) {
@@ -274,6 +282,7 @@ export const PaymentForm = ({
selectedTokenData,
isExternalWalletConnected,
isExternalWalletFlow,
+ showRequestPotInitialView,
currentView,
isProcessing,
])
@@ -323,6 +332,11 @@ export const PaymentForm = ({
const recipientExists = !!recipient
const walletConnected = isConnected
+ // If its requestPotPayment, we only need to check if the recipient exists, amount is set, and token is selected
+ if (showRequestPotInitialView) {
+ return recipientExists && amountIsSet && tokenSelected && showRequestPotInitialView
+ }
+
return recipientExists && amountIsSet && tokenSelected && walletConnected
}, [
recipient,
@@ -365,7 +379,8 @@ export const PaymentForm = ({
if (inviteError) {
setInviteError(false)
}
- if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) {
+ // Invites will be handled in the payment page, skip this step for request pots initial view
+ if (!showRequestPotInitialView && isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) {
// If the user doesn't have app access, accept the invite before claiming the link
if (recipient.recipientType === 'USERNAME' && !user?.user.hasAppAccess) {
const isAccepted = await handleAcceptInvite()
@@ -375,12 +390,14 @@ export const PaymentForm = ({
return
}
- if (!isExternalWalletConnected && isExternalWalletFlow) {
+ // skip this step for request pots initial view
+ if (!showRequestPotInitialView && !isExternalWalletConnected && isExternalWalletFlow) {
openReownModal()
return
}
- if (!isConnected) {
+ // skip this step for request pots initial view
+ if (!showRequestPotInitialView && !isConnected) {
dispatch(walletActions.setSignInModalVisible(true))
return
}
@@ -437,6 +454,7 @@ export const PaymentForm = ({
? 'DIRECT_SEND'
: 'REQUEST',
attachmentOptions: attachmentOptions,
+ returnAfterChargeCreation: !!showRequestPotInitialView, // For request pot initial view, return after charge creation without initiating payment
}
console.log('Initiating payment with payload:', payload)
@@ -446,7 +464,7 @@ export const PaymentForm = ({
if (result.status === 'Success') {
dispatch(paymentActions.setView('STATUS'))
} else if (result.status === 'Charge Created') {
- if (!fulfillUsingManteca) {
+ if (!fulfillUsingManteca && !showRequestPotInitialView) {
dispatch(paymentActions.setView('CONFIRM'))
}
} else if (result.status === 'Error') {
@@ -473,6 +491,7 @@ export const PaymentForm = ({
requestedTokenPrice,
inviteError,
handleAcceptInvite,
+ showRequestPotInitialView,
])
const getButtonText = () => {
@@ -488,6 +507,10 @@ export const PaymentForm = ({
return 'Send'
}
+ if (showRequestPotInitialView) {
+ return 'Pay'
+ }
+
if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) {
return (
@@ -516,11 +539,13 @@ export const PaymentForm = ({
}
const getButtonIcon = (): IconName | undefined => {
- if (!isExternalWalletConnected && isExternalWalletFlow) return 'wallet-outline'
+ if (!showRequestPotInitialView && !isExternalWalletConnected && isExternalWalletFlow) return 'wallet-outline'
- if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) return 'arrow-down'
+ if (!showRequestPotInitialView && isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow)
+ return 'arrow-down'
- if (!isProcessing && isActivePeanutWallet && !isExternalWalletFlow) return 'arrow-up-right'
+ if (!showRequestPotInitialView && !isProcessing && isActivePeanutWallet && !isExternalWalletFlow)
+ return 'arrow-up-right'
return undefined
}
@@ -613,13 +638,19 @@ export const PaymentForm = ({
}
}
+ const contributors = getContributorsFromCharge(requestDetails?.charges || [])
+
+ const totalAmountCollected = requestDetails?.totalCollectedAmount ?? 0
+
if (fulfillUsingManteca && chargeDetails) {
return
}
return (
-
+ {!showRequestPotInitialView && (
+
+ )}
{isExternalWalletConnected && isUsingExternalWallet && (
)}
@@ -666,11 +700,17 @@ export const PaymentForm = ({
}}
setCurrencyAmount={setCurrencyAmount}
className="w-full"
- disabled={!isExternalWalletFlow && (!!requestDetails?.tokenAmount || !!chargeDetails?.tokenAmount)}
+ disabled={
+ !showRequestPotInitialView &&
+ !isExternalWalletFlow &&
+ (!!requestDetails?.tokenAmount || !!chargeDetails?.tokenAmount)
+ }
walletBalance={isActivePeanutWallet ? peanutWalletBalance : undefined}
currency={currency}
hideCurrencyToggle={!currency}
hideBalance={isExternalWalletFlow}
+ showSlider={showRequestPotInitialView && amount ? Number(amount) > 0 : false}
+ maxAmount={showRequestPotInitialView && amount ? Number(amount) : undefined}
/>
{/*
@@ -708,7 +748,8 @@ export const PaymentForm = ({
)}
- {isPeanutWalletConnected && (!error || isInsufficientBalanceError) && (
+ {(showRequestPotInitialView ||
+ (isPeanutWalletConnected && (!error || isInsufficientBalanceError))) && (
+
+ {showRequestPotInitialView && (
+
+
Contributors ({contributors.length})
+ {contributors.map((contributor, index) => (
+
+ ))}
+
+ )}
+
setDisconnectWagmiModal(false)}
diff --git a/src/components/Profile/AvatarWithBadge.tsx b/src/components/Profile/AvatarWithBadge.tsx
index 348778681..ff21ba9b0 100644
--- a/src/components/Profile/AvatarWithBadge.tsx
+++ b/src/components/Profile/AvatarWithBadge.tsx
@@ -19,8 +19,6 @@ interface AvatarWithBadgeProps {
inlineStyle?: React.CSSProperties // for dynamic background colors based on username (hex codes)
textColor?: string
iconFillColor?: string
- showStatusPill?: boolean
- statusPillStatus?: StatusPillType
logo?: StaticImageData
}
@@ -36,8 +34,6 @@ const AvatarWithBadge: React.FC = ({
inlineStyle,
textColor,
iconFillColor,
- showStatusPill,
- statusPillStatus,
logo,
}) => {
const sizeClasses: Record = {
@@ -117,8 +113,6 @@ const AvatarWithBadge: React.FC = ({
initials
)}
-
- {showStatusPill && statusPillStatus &&
}
)
}
diff --git a/src/components/Request/link/views/Create.request.link.view.tsx b/src/components/Request/link/views/Create.request.link.view.tsx
index bfac90ad3..c2c94083a 100644
--- a/src/components/Request/link/views/Create.request.link.view.tsx
+++ b/src/components/Request/link/views/Create.request.link.view.tsx
@@ -23,7 +23,7 @@ import { fetchTokenSymbol, getRequestLink, isNativeCurrency, printableUsdc } fro
import * as Sentry from '@sentry/nextjs'
import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk'
import { useQueryClient } from '@tanstack/react-query'
-import { useRouter } from 'next/navigation'
+import { useRouter, useSearchParams } from 'next/navigation'
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
export const CreateRequestLinkView = () => {
@@ -41,11 +41,16 @@ export const CreateRequestLinkView = () => {
} = useContext(context.tokenSelectorContext)
const { setLoadingState } = useContext(context.loadingStateContext)
const queryClient = useQueryClient()
+ const searchParams = useSearchParams()
+ const paramsAmount = searchParams.get('amount')
+ const sanitizedAmount = paramsAmount && !isNaN(parseFloat(paramsAmount)) ? paramsAmount : ''
+ const merchant = searchParams.get('merchant')
+ const merchantComment = merchant ? `Bill split for ${merchant}` : null
// Core state
- const [tokenValue, setTokenValue] = useState
('')
+ const [tokenValue, setTokenValue] = useState(sanitizedAmount)
const [attachmentOptions, setAttachmentOptions] = useState({
- message: '',
+ message: merchantComment || '',
fileUrl: '',
rawFile: undefined,
})
@@ -109,15 +114,6 @@ export const CreateRequestLinkView = () => {
})
return null
}
-
- if (!tokenValue || parseFloat(tokenValue) <= 0) {
- setErrorState({
- showError: true,
- errorMessage: 'Please enter a token amount',
- })
- return null
- }
-
// Cleanup previous request
if (createLinkAbortRef.current) {
createLinkAbortRef.current.abort()
@@ -169,21 +165,8 @@ export const CreateRequestLinkView = () => {
const requestDetails = await requestsApi.create(requestData)
setRequestId(requestDetails.uuid)
- const charge = await chargesApi.create({
- pricing_type: 'fixed_price',
- local_price: {
- amount: requestDetails.tokenAmount,
- currency: 'USD',
- },
- baseUrl: BASE_URL,
- requestId: requestDetails.uuid,
- transactionType: 'REQUEST',
- })
-
const link = getRequestLink({
...requestDetails,
- uuid: undefined,
- chargeId: charge.data.id,
})
// Update the last saved state
@@ -277,17 +260,7 @@ export const CreateRequestLinkView = () => {
const handleDebouncedChange = useCallback(async () => {
if (isCreatingLink || isUpdatingRequest) return
- // If no request exists but we have content, create request
- if (!requestId && (debouncedAttachmentOptions.rawFile || debouncedAttachmentOptions.message)) {
- if (!tokenValue || parseFloat(tokenValue) <= 0) return
-
- const link = await createRequestLink(debouncedAttachmentOptions)
- if (link) {
- setGeneratedLink(link)
- }
- }
- // If request exists and content changed (including clearing), update it
- else if (requestId) {
+ if (requestId) {
// Check for unsaved changes inline to avoid dependency issues
const lastSaved = lastSavedAttachmentRef.current
const hasChanges =
@@ -298,15 +271,7 @@ export const CreateRequestLinkView = () => {
await updateRequestLink(debouncedAttachmentOptions)
}
}
- }, [
- debouncedAttachmentOptions,
- requestId,
- tokenValue,
- isCreatingLink,
- isUpdatingRequest,
- createRequestLink,
- updateRequestLink,
- ])
+ }, [debouncedAttachmentOptions, requestId, isCreatingLink, isUpdatingRequest, updateRequestLink])
useEffect(() => {
handleDebouncedChange()
@@ -355,7 +320,6 @@ export const CreateRequestLinkView = () => {
const generateLink = useCallback(async () => {
if (generatedLink) return generatedLink
- if (Number(tokenValue) === 0) return qrCodeLink
if (isCreatingLink || isUpdatingRequest) return '' // Prevent duplicate operations
// Create new request when share button is clicked
@@ -376,7 +340,7 @@ export const CreateRequestLinkView = () => {
return (
-
router.push('/request')} title="Request" />
+ router.push('/home')} title="Request" />
@@ -389,6 +353,8 @@ export const CreateRequestLinkView = () => {
onSubmit={handleTokenAmountSubmit}
walletBalance={peanutWalletBalance}
disabled={!!requestId}
+ showInfoText
+ infoText="Leave empty to let payers choose amounts."
/>
{
) : (
- Share Link
+
+ {!tokenValue || !parseFloat(tokenValue) || parseFloat(tokenValue) === 0
+ ? 'Share open request'
+ : `Share $${tokenValue} request`}
+
)}
{errorState.showError && (
diff --git a/src/components/Request/views/RequestRouter.view.tsx b/src/components/Request/views/RequestRouter.view.tsx
deleted file mode 100644
index 020430213..000000000
--- a/src/components/Request/views/RequestRouter.view.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-'use client'
-import RouterViewWrapper from '@/components/RouterViewWrapper'
-import { useRouter } from 'next/navigation'
-
-export const RequestRouterView = () => {
- const router = useRouter()
-
- return (
- router.push('/request/create')}
- onUserSelect={(username) => router.push(`/request/${username}`)}
- />
- )
-}
diff --git a/src/components/TransactionDetails/TransactionAvatarBadge.tsx b/src/components/TransactionDetails/TransactionAvatarBadge.tsx
index 8fc4a8935..bc18e111e 100644
--- a/src/components/TransactionDetails/TransactionAvatarBadge.tsx
+++ b/src/components/TransactionDetails/TransactionAvatarBadge.tsx
@@ -19,7 +19,6 @@ interface TransactionAvatarBadgeProps {
isLinkTransaction?: boolean
transactionType: TransactionType
context: 'card' | 'header' | 'drawer'
- status?: StatusPillType
}
/**
@@ -33,7 +32,6 @@ const TransactionAvatarBadge: React.FC = ({
size = 'medium',
transactionType,
context,
- status,
}) => {
let displayIconName: IconName | undefined = undefined
let displayInitials: string | undefined = initials
@@ -113,8 +111,6 @@ const TransactionAvatarBadge: React.FC = ({
inlineStyle={{ backgroundColor: calculatedBgColor }}
textColor={textColor}
iconFillColor={iconFillColor}
- showStatusPill
- statusPillStatus={status}
/>
)
}
diff --git a/src/components/TransactionDetails/TransactionCard.tsx b/src/components/TransactionDetails/TransactionCard.tsx
index 8142f8aef..83654e410 100644
--- a/src/components/TransactionDetails/TransactionCard.tsx
+++ b/src/components/TransactionDetails/TransactionCard.tsx
@@ -89,8 +89,16 @@ const TransactionCard: React.FC = ({
usdAmount = Number(transaction.currency?.amount ?? amount)
}
- const formattedAmount = formatCurrency(Math.abs(usdAmount).toString())
- const displayAmount = `${sign}$${formattedAmount}`
+ const formattedAmount = formatCurrency(Math.abs(usdAmount).toString(), 2, 0)
+ const formattedTotalAmountCollected = formatCurrency(transaction.totalAmountCollected.toString(), 2, 0)
+
+ let displayAmount = `${sign}$${formattedAmount}`
+
+ if (transaction.isRequestPotLink && Number(transaction.amount) > 0) {
+ displayAmount = `$${formattedTotalAmountCollected} / $${formattedAmount}`
+ } else if (transaction.isRequestPotLink && Number(transaction.amount) === 0) {
+ displayAmount = `$${formattedTotalAmountCollected}`
+ }
let currencyDisplayAmount: string | undefined
if (transaction.currency && transaction.currency.code.toUpperCase() !== 'USD') {
@@ -105,36 +113,35 @@ const TransactionCard: React.FC = ({
{/* txn avatar component handles icon/initials/colors */}
-
- {isPerkReward ? (
- <>
-
- {status &&
}
- >
- ) : avatarUrl ? (
-
-
-
- {status && }
-
- ) : (
-
*/}
+ {isPerkReward ? (
+ <>
+
+ {status && }
+ >
+ ) : avatarUrl ? (
+
+
- )}
-
+
+ {status && }
+
+ ) : (
+
+ )}
{/* display formatted name (address or username) */}
@@ -149,9 +156,10 @@ const TransactionCard: React.FC = ({
{/* display the action icon and type text */}
-
+
{getActionIcon(type, transaction.direction)}
{isPerkReward ? 'Refund' : getActionText(type)}
+ {status && }
@@ -183,7 +191,7 @@ const TransactionCard: React.FC
= ({
// helper functions
function getActionIcon(type: TransactionType, direction: TransactionDirection): React.ReactNode {
let iconName: IconName | null = null
- let iconSize = 8
+ let iconSize = 7
switch (type) {
case 'send':
diff --git a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx
index 47eb44b75..bfd88be9f 100644
--- a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx
+++ b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx
@@ -10,6 +10,7 @@ import { isAddress as isWalletAddress } from 'viem'
import Card from '../Global/Card'
import { Icon, type IconName } from '../Global/Icons/Icon'
import { VerifiedUserLabel } from '../UserHeader'
+import ProgressBar from '../Global/ProgressBar'
import { useRouter } from 'next/navigation'
import { twMerge } from 'tailwind-merge'
@@ -39,6 +40,11 @@ interface TransactionDetailsHeaderCardProps {
avatarUrl?: string
haveSentMoneyToUser?: boolean
isAvatarClickable?: boolean
+ showProgessBar?: boolean
+ progress?: number
+ goal?: number
+ isRequestPotTransaction?: boolean
+ isTransactionClosed: boolean
}
const getTitle = (
@@ -170,6 +176,11 @@ export const TransactionDetailsHeaderCard: React.FC {
const router = useRouter()
const typeForAvatar =
@@ -183,6 +194,8 @@ export const TransactionDetailsHeaderCard: React.FC
@@ -204,31 +217,49 @@ export const TransactionDetailsHeaderCard: React.FC
)}
-
+
{icon && }
+
+
+ {status && }
+
{amountDisplay}
+
+ {isNoGoalSet &&
No goal set }
- {status && }
+ {!isNoGoalSet && showProgessBar && goal !== undefined && progress !== undefined && (
+
+ )}
)
}
diff --git a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx
index 1b45685c1..e7053c956 100644
--- a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx
+++ b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx
@@ -7,7 +7,7 @@
* - Large component that could be split into smaller focused components
*/
-import Card from '@/components/Global/Card'
+import Card, { getCardPosition } from '@/components/Global/Card'
import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow'
import { type TransactionDetails } from '@/components/TransactionDetails/transactionTransformer'
import { TRANSACTIONS } from '@/constants/query.consts'
@@ -17,7 +17,14 @@ import { useUserStore } from '@/redux/hooks'
import { chargesApi } from '@/services/charges'
import useClaimLink from '@/components/Claim/useClaimLink'
import { formatAmount, formatDate, getInitialsFromName, isStableCoin, formatCurrency, getAvatarUrl } from '@/utils'
-import { formatIban, printableAddress, shortenAddress, shortenStringLong, slugify } from '@/utils/general.utils'
+import {
+ formatIban,
+ getContributorsFromCharge,
+ printableAddress,
+ shortenAddress,
+ shortenStringLong,
+ slugify,
+} from '@/utils/general.utils'
import { cancelOnramp } from '@/app/actions/onramp'
import { captureException } from '@sentry/nextjs'
import { useQueryClient } from '@tanstack/react-query'
@@ -54,6 +61,9 @@ import {
import { mantecaApi } from '@/services/manteca'
import { getReceiptUrl } from '@/utils/history.utils'
import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants'
+import TransactionCard from './TransactionCard'
+import ContributorCard from '../Global/Contributors/ContributorCard'
+import { requestsApi } from '@/services/requests'
export const TransactionDetailsReceipt = ({
transaction,
@@ -187,6 +197,7 @@ export const TransactionDetailsReceipt = ({
!isPublic &&
transaction.extraDataForDrawer?.originalType === EHistoryEntryType.MANTECA_ONRAMP &&
transaction.status === 'pending',
+ closed: !!(transaction.status === 'closed' && transaction.cancelledDate),
}
}, [transaction, isPendingBankRequest])
@@ -251,6 +262,19 @@ export const TransactionDetailsReceipt = ({
return false
}, [transaction, isPendingSentLink, isPendingRequester, isPendingRequestee])
+ const isQRPayment =
+ transaction &&
+ [EHistoryEntryType.MANTECA_QR_PAYMENT, EHistoryEntryType.SIMPLEFI_QR_PAYMENT].includes(
+ transaction.extraDataForDrawer!.originalType
+ )
+
+ const requestPotContributors = useMemo(() => {
+ if (!transaction || !transaction.requestPotPayments) return []
+ return getContributorsFromCharge(transaction.requestPotPayments)
+ }, [transaction])
+
+ const formattedTotalAmountCollected = formatCurrency(transaction?.totalAmountCollected?.toString() ?? '0', 2, 0)
+
useEffect(() => {
const getTokenDetails = async () => {
if (!transaction) {
@@ -313,7 +337,7 @@ export const TransactionDetailsReceipt = ({
// ensure we have a valid number for display
const numericAmount = typeof usdAmount === 'bigint' ? Number(usdAmount) : usdAmount
const safeAmount = isNaN(numericAmount) || numericAmount === null || numericAmount === undefined ? 0 : numericAmount
- const amountDisplay = `$ ${formatCurrency(Math.abs(safeAmount).toString())}`
+ let amountDisplay = `$${formatCurrency(Math.abs(safeAmount).toString())}`
const feeDisplay = transaction.fee !== undefined ? formatAmount(transaction.fee as number) : 'N/A'
@@ -337,6 +361,12 @@ export const TransactionDetailsReceipt = ({
}
}
+ if (transaction.isRequestPotLink && Number(transaction.amount) > 0) {
+ amountDisplay = `$${formatCurrency(transaction.amount.toString())}`
+ } else if (transaction.isRequestPotLink && Number(transaction.amount) === 0) {
+ amountDisplay = `$${formattedTotalAmountCollected} collected`
+ }
+
// Show profile button only if txn is completed, not to/by a guest user and its a send/request/receive txn
const isAvatarClickable =
!!transaction &&
@@ -347,6 +377,27 @@ export const TransactionDetailsReceipt = ({
transaction.extraDataForDrawer?.transactionCardType === 'request' ||
transaction.extraDataForDrawer?.transactionCardType === 'receive')
+ const closeRequestLink = async () => {
+ if (isPendingRequester && setIsLoading && onClose) {
+ setIsLoading(true)
+ try {
+ if (transaction.isRequestPotLink) {
+ await requestsApi.close(transaction.id)
+ } else {
+ await chargesApi.cancel(transaction.id)
+ }
+ await queryClient.invalidateQueries({
+ queryKey: [TRANSACTIONS],
+ })
+ setIsLoading(false)
+ onClose()
+ } catch (error) {
+ captureException(error)
+ console.error('Error canceling charge:', error)
+ setIsLoading(false)
+ }
+ }
+ }
// Special rendering for PERK_REWARD type
const isPerkReward = transaction.extraDataForDrawer?.originalType === EHistoryEntryType.PERK_REWARD
const perkRewardData = transaction.extraDataForDrawer?.perkReward
@@ -456,6 +507,11 @@ export const TransactionDetailsReceipt = ({
avatarUrl={avatarUrl ?? getAvatarUrl(transaction)}
haveSentMoneyToUser={transaction.haveSentMoneyToUser}
isAvatarClickable={isAvatarClickable}
+ showProgessBar={transaction.isRequestPotLink}
+ goal={Number(transaction.amount)}
+ progress={Number(formattedTotalAmountCollected)}
+ isRequestPotTransaction={transaction.isRequestPotLink}
+ isTransactionClosed={transaction.status === 'closed'}
/>
{/* Perk eligibility banner */}
@@ -607,6 +663,18 @@ export const TransactionDetailsReceipt = ({
>
)}
+ {rowVisibilityConfig.closed && (
+ <>
+ {transaction.cancelledDate && (
+
+ )}
+ >
+ )}
+
{rowVisibilityConfig.claimed && (
<>
{transaction.claimedAt && (
@@ -1068,31 +1136,12 @@ export const TransactionDetailsReceipt = ({
iconClassName="p-1"
loading={isLoading}
disabled={isLoading}
- onClick={() => {
- setIsLoading(true)
- chargesApi
- .cancel(transaction.id)
- .then(() => {
- queryClient
- .invalidateQueries({
- queryKey: [TRANSACTIONS],
- })
- .then(() => {
- setIsLoading(false)
- onClose()
- })
- })
- .catch((error) => {
- captureException(error)
- console.error('Error canceling charge:', error)
- setIsLoading(false)
- })
- }}
+ onClick={closeRequestLink}
variant={'primary-soft'}
shadowSize="4"
className="flex w-full items-center gap-1"
>
- Cancel request
+ {transaction.totalAmountCollected > 0 ? 'Close request' : 'Cancel request'}
)}
@@ -1143,9 +1192,23 @@ export const TransactionDetailsReceipt = ({
)}
+ {isQRPayment && (
+ {
+ router.push(`/request?amount=${transaction.amount}&merchant=${transaction.userName}`)
+ }}
+ icon="split"
+ shadowSize="4"
+ >
+ Split this bill
+
+ )}
+
{shouldShowShareReceipt && !!getReceiptUrl(transaction) && (
- Share Receipt
+
+ Share Receipt
+
)}
@@ -1360,6 +1423,21 @@ export const TransactionDetailsReceipt = ({
}}
/>
)}
+
+ {requestPotContributors.length > 0 && (
+ <>
+ Contributors ({requestPotContributors.length})
+
+ {requestPotContributors.map((contributor, index) => (
+
+ ))}
+
+ >
+ )}
)
}
diff --git a/src/components/TransactionDetails/transaction-details.utils.ts b/src/components/TransactionDetails/transaction-details.utils.ts
index 09462bea5..73fa42274 100644
--- a/src/components/TransactionDetails/transaction-details.utils.ts
+++ b/src/components/TransactionDetails/transaction-details.utils.ts
@@ -18,6 +18,7 @@ export type TransactionDetailsRowKey =
| 'comment'
| 'attachment'
| 'mantecaDepositInfo'
+ | 'closed'
// order of the rows in the receipt
export const transactionDetailsRowKeys: TransactionDetailsRowKey[] = [
@@ -38,6 +39,7 @@ export const transactionDetailsRowKeys: TransactionDetailsRowKey[] = [
'comment',
'networkFee',
'attachment',
+ 'closed',
]
export const getBankAccountLabel = (type: string) => {
diff --git a/src/components/TransactionDetails/transactionTransformer.ts b/src/components/TransactionDetails/transactionTransformer.ts
index 45d578ae7..3693d7c0b 100644
--- a/src/components/TransactionDetails/transactionTransformer.ts
+++ b/src/components/TransactionDetails/transactionTransformer.ts
@@ -13,7 +13,7 @@ import {
import { type StatusPillType } from '../Global/StatusPill'
import type { Address } from 'viem'
import { PEANUT_WALLET_CHAIN } from '@/constants'
-import { type HistoryEntryPerkReward } from '@/services/services.types'
+import { type HistoryEntryPerkReward, type ChargeEntry } from '@/services/services.types'
/**
* @fileoverview maps raw transaction history data from the api/hook to the format needed by ui components.
@@ -130,6 +130,9 @@ export interface TransactionDetails {
createdAt?: string | Date
completedAt?: string | Date
points?: number
+ isRequestPotLink?: boolean
+ requestPotPayments?: ChargeEntry[]
+ totalAmountCollected: number
}
/**
@@ -411,6 +414,10 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact
case 'EXPIRED':
uiStatus = 'cancelled'
break
+ case 'CLOSED':
+ // If the total amount collected is 0, the link is treated as cancelled
+ uiStatus = entry.totalAmountCollected === 0 ? 'cancelled' : 'closed'
+ break
default:
{
const knownStatuses: StatusType[] = [
@@ -539,6 +546,9 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact
createdAt: entry.createdAt,
completedAt: entry.completedAt,
haveSentMoneyToUser: entry.extraData?.haveSentMoneyToUser as boolean,
+ isRequestPotLink: entry.isRequestLink,
+ requestPotPayments: entry.charges,
+ totalAmountCollected: entry.totalAmountCollected ?? 0,
}
return {
diff --git a/src/components/User/UserCard.tsx b/src/components/User/UserCard.tsx
index 8bb913521..e0a96ecb8 100644
--- a/src/components/User/UserCard.tsx
+++ b/src/components/User/UserCard.tsx
@@ -7,9 +7,11 @@ import Card from '../Global/Card'
import { Icon, type IconName } from '../Global/Icons/Icon'
import AvatarWithBadge, { type AvatarSize } from '../Profile/AvatarWithBadge'
import { VerifiedUserLabel } from '../UserHeader'
+import { twMerge } from 'tailwind-merge'
+import ProgressBar from '../Global/ProgressBar'
interface UserCardProps {
- type: 'send' | 'request' | 'received_link'
+ type: 'send' | 'request' | 'received_link' | 'request_pay'
username: string
fullName?: string
recipientType?: RecipientType
@@ -18,6 +20,9 @@ interface UserCardProps {
fileUrl?: string
isVerified?: boolean
haveSentMoneyToUser?: boolean
+ amount?: number
+ amountCollected?: number
+ isRequestPot?: boolean
}
const UserCard = ({
@@ -30,6 +35,9 @@ const UserCard = ({
fileUrl,
isVerified,
haveSentMoneyToUser,
+ amount,
+ amountCollected,
+ isRequestPot,
}: UserCardProps) => {
const getIcon = (): IconName | undefined => {
if (type === 'send') return 'arrow-up-right'
@@ -43,6 +51,7 @@ const UserCard = ({
if (type === 'send') title = `You're sending money to`
if (type === 'request') title = `Requesting money from`
if (type === 'received_link') title = `You received`
+ if (type === 'request_pay') title = `${fullName ?? username} is requesting`
return (
@@ -51,36 +60,58 @@ const UserCard = ({
)
}, [type])
+ const getAddressLinkTitle = () => {
+ if (isRequestPot && amount && amount > 0) return `$${amount}` // If goal is set.
+ if (!amount && isRequestPot) return `Pay what you want` // If no goal is set.
+
+ return username
+ }
+
return (
-
-
-
- {getTitle()}
- {recipientType !== 'USERNAME' ? (
-
- ) : (
-
- )}
-
+
+
+
+
+ {getTitle()}
+ {recipientType !== 'USERNAME' || type === 'request_pay' ? (
+
+ ) : (
+
+ )}
+
+
+ {amount !== undefined && amountCollected !== undefined && type === 'request_pay' && amount > 0 && (
+ = amount} />
+ )}
)
}
diff --git a/src/hooks/usePaymentInitiator.ts b/src/hooks/usePaymentInitiator.ts
index e783d56cd..b6e2497e5 100644
--- a/src/hooks/usePaymentInitiator.ts
+++ b/src/hooks/usePaymentInitiator.ts
@@ -25,6 +25,7 @@ import { waitForTransactionReceipt } from 'wagmi/actions'
import { getRoute, type PeanutCrossChainRoute } from '@/services/swap'
import { estimateTransactionCostUsd } from '@/app/actions/tokens'
import { captureException } from '@sentry/nextjs'
+import { useRouter } from 'next/navigation'
enum ELoadingStep {
IDLE = 'Idle',
@@ -57,6 +58,7 @@ export interface InitiatePaymentPayload {
isExternalWalletFlow?: boolean
transactionType?: TChargeTransactionType
attachmentOptions?: IAttachmentOptions
+ returnAfterChargeCreation?: boolean
}
interface InitiationResult {
@@ -77,6 +79,7 @@ export const usePaymentInitiator = () => {
const { switchChainAsync } = useSwitchChain()
const { address: wagmiAddress } = useAppKitAccount()
const { sendTransactionAsync } = useSendTransaction()
+ const router = useRouter()
const config = useConfig()
const { chain: connectedWalletChain } = useWagmiAccount()
@@ -158,7 +161,9 @@ export const usePaymentInitiator = () => {
if (currentUrl.searchParams.get('chargeId') === activeChargeDetails.uuid) {
const newUrl = new URL(window.location.href)
newUrl.searchParams.delete('chargeId')
- window.history.replaceState(null, '', newUrl.toString())
+ // Use router.push (not window.history.replaceState) so that
+ // the components using the search params will be updated
+ router.push(newUrl.pathname + newUrl.search)
}
}
return {
@@ -168,7 +173,7 @@ export const usePaymentInitiator = () => {
success: false,
}
},
- [activeChargeDetails]
+ [activeChargeDetails, router]
)
// prepare transaction details (called from Confirm view)
@@ -391,11 +396,9 @@ export const usePaymentInitiator = () => {
const newUrl = new URL(window.location.href)
if (payload.requestId) newUrl.searchParams.delete('id')
newUrl.searchParams.set('chargeId', chargeDetailsToUse.uuid)
- window.history.replaceState(
- { ...window.history.state, as: newUrl.href, url: newUrl.href },
- '',
- newUrl.href
- )
+ // Use router.push (not window.history.replaceState) so that
+ // the components using the search params will be updated
+ router.push(newUrl.pathname + newUrl.search)
console.log('Updated URL with chargeId:', newUrl.href)
}
}
@@ -619,12 +622,13 @@ export const usePaymentInitiator = () => {
// 2. handle charge state
if (
- chargeCreated &&
- (payload.isExternalWalletFlow ||
- !isPeanutWallet ||
- (isPeanutWallet &&
- (!areEvmAddressesEqual(determinedChargeDetails.tokenAddress, PEANUT_WALLET_TOKEN) ||
- determinedChargeDetails.chainId !== PEANUT_WALLET_CHAIN.id.toString())))
+ payload.returnAfterChargeCreation || // For request pot payment, return after charge creation
+ (chargeCreated &&
+ (payload.isExternalWalletFlow ||
+ !isPeanutWallet ||
+ (isPeanutWallet &&
+ (!areEvmAddressesEqual(determinedChargeDetails.tokenAddress, PEANUT_WALLET_TOKEN) ||
+ determinedChargeDetails.chainId !== PEANUT_WALLET_CHAIN.id.toString()))))
) {
console.log(
`Charge created. Transitioning to Confirm view for: ${
@@ -673,7 +677,9 @@ export const usePaymentInitiator = () => {
if (currentUrl.searchParams.get('chargeId') === determinedChargeDetails.uuid) {
const newUrl = new URL(window.location.href)
newUrl.searchParams.delete('chargeId')
- window.history.replaceState(null, '', newUrl.toString())
+ // Use router.push (not window.history.replaceState) so that
+ // the components using the search params will be updated
+ router.push(newUrl.pathname + newUrl.search)
console.log('URL updated, chargeId removed.')
}
}
@@ -691,6 +697,7 @@ export const usePaymentInitiator = () => {
handleError,
setLoadingStep,
setError,
+ router,
setTransactionHash,
setPaymentDetails,
loadingStep,
diff --git a/src/services/requests.ts b/src/services/requests.ts
index 6c6089daf..f7932af47 100644
--- a/src/services/requests.ts
+++ b/src/services/requests.ts
@@ -1,20 +1,21 @@
import { PEANUT_API_URL } from '@/constants'
import { type CreateRequestRequest, type TRequestResponse } from './services.types'
-import { fetchWithSentry } from '@/utils'
+import { fetchWithSentry, jsonStringify } from '@/utils'
+import Cookies from 'js-cookie'
export const requestsApi = {
create: async (data: CreateRequestRequest): Promise
=> {
- const formData = new FormData()
-
- Object.entries(data).forEach(([key, value]) => {
- if (value !== undefined) {
- formData.append(key, value)
- }
- })
-
- const response = await fetchWithSentry('/api/proxy/withFormData/requests', {
+ const token = Cookies.get('jwt-token')
+ if (!token) {
+ throw new Error('Authentication token not found. Please log in again.')
+ }
+ const response = await fetchWithSentry(`${PEANUT_API_URL}/requests`, {
method: 'POST',
- body: formData,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ body: jsonStringify(data),
})
if (!response.ok) {
@@ -37,17 +38,17 @@ export const requestsApi = {
},
update: async (id: string, data: Partial): Promise => {
- const formData = new FormData()
-
- Object.entries(data).forEach(([key, value]) => {
- if (value !== undefined) {
- formData.append(key, value)
- }
- })
-
- const response = await fetchWithSentry(`/api/proxy/withFormData/requests/${id}`, {
+ const token = Cookies.get('jwt-token')
+ if (!token) {
+ throw new Error('Authentication token not found. Please log in again.')
+ }
+ const response = await fetchWithSentry(`${PEANUT_API_URL}/requests/${id}`, {
method: 'PATCH',
- body: formData,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ body: jsonStringify(data),
})
if (!response.ok) {
@@ -85,4 +86,21 @@ export const requestsApi = {
}
return response.json()
},
+
+ close: async (uuid: string): Promise => {
+ const token = Cookies.get('jwt-token')
+ if (!token) {
+ throw new Error('Authentication token not found. Please log in again.')
+ }
+ const response = await fetchWithSentry(`${PEANUT_API_URL}/requests/${uuid}`, {
+ method: 'DELETE',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ })
+ if (!response.ok) {
+ throw new Error(`Failed to close request: ${response.statusText}`)
+ }
+ return response.json()
+ },
}
diff --git a/src/services/services.types.ts b/src/services/services.types.ts
index a3c39e1a7..7313559b3 100644
--- a/src/services/services.types.ts
+++ b/src/services/services.types.ts
@@ -19,9 +19,6 @@ export interface CreateRequestRequest {
tokenAddress: string
tokenDecimals: string
tokenSymbol: string
- attachment?: File
- mimeType?: string
- filename?: string
}
export interface TRequestResponse {
@@ -48,6 +45,7 @@ export interface TRequestResponse {
username: string
}
}
+ totalCollectedAmount: number
}
export interface ChargeEntry {
diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts
index 6065f2433..d488f726b 100644
--- a/src/utils/general.utils.ts
+++ b/src/utils/general.utils.ts
@@ -10,6 +10,7 @@ import { getAddress, isAddress, erc20Abi } from 'viem'
import * as wagmiChains from 'wagmi/chains'
import { getPublicClient, type ChainId } from '@/app/actions/clients'
import { NATIVE_TOKEN_ADDRESS, SQUID_ETH_ADDRESS } from './token.utils'
+import { type ChargeEntry } from '@/services/services.types'
export function urlBase64ToUint8Array(base64String: string) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
@@ -377,8 +378,8 @@ export const formatNumberForDisplay = (
})
}
-export function formatCurrency(valueStr: string | undefined): string {
- return formatNumberForDisplay(valueStr, { maxDecimals: 2, minDecimals: 2 })
+export function formatCurrency(valueStr: string | undefined, maxDecimals: number = 2, minDecimals: number = 2): string {
+ return formatNumberForDisplay(valueStr, { maxDecimals, minDecimals })
}
/**
@@ -1357,3 +1358,22 @@ export const getValidRedirectUrl = (redirectUrl: string, fallbackRoute: string)
return fallbackRoute
}
}
+
+export const getContributorsFromCharge = (charges: ChargeEntry[]) => {
+ return charges.map((charge) => {
+ const successfulPayment = charge.payments.at(-1)
+ let username = successfulPayment?.payerAccount?.user?.username
+ if (successfulPayment?.payerAccount?.type === 'evm-address') {
+ username = successfulPayment.payerAccount.identifier
+ }
+
+ return {
+ uuid: charge.uuid,
+ payments: charge.payments,
+ amount: charge.tokenAmount,
+ username,
+ fulfillmentPayment: charge.fulfillmentPayment,
+ isUserVerified: successfulPayment?.payerAccount?.user?.bridgeKycStatus === 'approved',
+ }
+ })
+}
diff --git a/src/utils/history.utils.ts b/src/utils/history.utils.ts
index 29adf76c9..8ee636cb4 100644
--- a/src/utils/history.utils.ts
+++ b/src/utils/history.utils.ts
@@ -6,6 +6,7 @@ import { formatUnits } from 'viem'
import { type Hash } from 'viem'
import { getTokenDetails } from '@/utils'
import { getCurrencyPrice } from '@/app/actions/currency'
+import { type ChargeEntry } from '@/services/services.types'
export enum EHistoryEntryType {
REQUEST = 'REQUEST',
@@ -71,6 +72,7 @@ export enum EHistoryStatus {
refunded = 'refunded',
canceled = 'canceled', // from simplefi, canceled with only one l
expired = 'expired',
+ CLOSED = 'CLOSED',
}
export const FINAL_STATES: HistoryStatus[] = [
@@ -81,6 +83,7 @@ export const FINAL_STATES: HistoryStatus[] = [
EHistoryStatus.REFUNDED,
EHistoryStatus.CANCELED,
EHistoryStatus.ERROR,
+ EHistoryStatus.CLOSED,
]
export type HistoryEntryType = `${EHistoryEntryType}`
@@ -129,6 +132,9 @@ export type HistoryEntry = {
completedAt?: string | Date
isVerified?: boolean
points?: number
+ isRequestLink?: boolean // true if the transaction is a request pot link
+ charges?: ChargeEntry[]
+ totalAmountCollected?: number
}
export function isFinalState(transaction: Pick): boolean {
@@ -219,7 +225,14 @@ export async function completeHistoryEntry(entry: HistoryEntry): Promise