From 8ea6e7d4d0ecce43059be330d7e0579a24b05cb8 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Sun, 12 Oct 2025 19:45:09 +0530 Subject: [PATCH 01/28] feat: add progress bar component --- src/components/Global/ProgressBar/index.tsx | 146 ++++++++++++++++++ .../TransactionDetailsHeaderCard.tsx | 14 +- tailwind.config.js | 1 + 3 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 src/components/Global/ProgressBar/index.tsx diff --git a/src/components/Global/ProgressBar/index.tsx b/src/components/Global/ProgressBar/index.tsx new file mode 100644 index 000000000..668fc8e6f --- /dev/null +++ b/src/components/Global/ProgressBar/index.tsx @@ -0,0 +1,146 @@ +import React from 'react' +import { twMerge } from 'tailwind-merge' + +interface ProgressBarProps { + goal: number + progress: number + isClosed: boolean +} + +const ProgressBar: React.FC = ({ goal, progress, isClosed }) => { + const isOverGoal = progress > goal + const isGoalAchieved = progress >= goal && !isOverGoal + const totalValue = isOverGoal ? progress : goal + + const goalPercentage = (goal / totalValue) * 100 + const progressPercentage = (progress / totalValue) * 100 + const percentage = Math.round((progress / goal) * 100) + + const formatCurrency = (value: number) => `$${value.toFixed(2)}` + + const getStatusText = () => { + if (isOverGoal) return 'Goal exceeded!' + if (isGoalAchieved) return 'Goal achieved!' + return `${100 - percentage}% below goal` + } + + const getBackgroundColor = () => { + if (!isClosed) return 'bg-grey-2' + return isGoalAchieved ? 'bg-success-3' : 'bg-error-4' + } + + const renderStatusText = () => { + if (!isClosed) return null + return ( +
+

{getStatusText()}

+
+ ) + } + + const renderLabels = () => { + if (!isClosed) { + return ( +
+

{formatCurrency(progress)} contributed

+

{formatCurrency(goal - progress)} remaining

+
+ ) + } + + if (isOverGoal) { + return ( +
+

+ 100% +

+

{formatCurrency(progress)}

+
+ ) + } + + if (isGoalAchieved) return null + + return ( +
+

+ {formatCurrency(progress)} +

+
+

{percentage}%

+
+
+ ) + } + + const renderMarker = (color: string, position: string | number, isPercentage = true) => { + const positionStyle = typeof position === 'string' ? { left: position } : { left: `${position}%` } + return ( +
+
+
+ ) + } + + const renderGoalMarker = () => { + const markerColor = isGoalAchieved ? 'bg-success-3' : 'bg-error-4' + const containerClasses = twMerge( + 'absolute top-1/2 z-10 -translate-y-1/2 transition-all duration-300', + isGoalAchieved ? 'right-0' : '-translate-x-1/2' + ) + const containerStyle = isGoalAchieved ? {} : { left: '100%' } + + return ( +
+ {isGoalAchieved &&

100%

} +
+
+ ) + } + + const renderProgressBar = () => { + const barBaseClasses = 'absolute left-0 h-1.5 rounded-full transition-all duration-300 ease-in-out' + + if (isOverGoal) { + return ( + <> +
+
+ {isClosed && ( + <> + {renderMarker('bg-success-3', goalPercentage)} + {renderMarker('bg-yellow-1', '100%', false)} + + )} + + ) + } + + return ( + <> +
+
+ {isClosed && ( + <> + {!isGoalAchieved && renderMarker('bg-success-3', progressPercentage)} + {renderGoalMarker()} + + )} + + ) + } + + return ( +
+ {renderStatusText()} + {renderLabels()} + +
{renderProgressBar()}
+
+ ) +} + +export default ProgressBar diff --git a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx index 2a9b9ca8b..879c33de5 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, IconName } from '../Global/Icons/Icon' import { VerifiedUserLabel } from '../UserHeader' +import ProgressBar from '../Global/ProgressBar' export type TransactionDirection = | 'send' @@ -195,8 +196,8 @@ export const TransactionDetailsHeaderCard: React.FC )} -
-

+
+

{icon && } + { +
+ {status && } +
+ }

-
{status && }
+
+ +
) } diff --git a/tailwind.config.js b/tailwind.config.js index 0dd19ed4b..e53e6a824 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -112,6 +112,7 @@ module.exports = { 1: '#FFD8D8', 2: '#EA8282', 3: '#FF4A4A', + 4: '#FC5555', }, }, zIndex: { From fce6615533ef435cbc82571d117269c7301fd49d Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Sun, 12 Oct 2025 19:52:11 +0530 Subject: [PATCH 02/28] feat: add contributors --- .../TransactionDetailsReceipt.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx index ba70d4e93..073d25547 100644 --- a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx +++ b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx @@ -1,6 +1,6 @@ 'use client' -import Card from '@/components/Global/Card' +import Card, { getCardPosition } from '@/components/Global/Card' import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow' import { TransactionDetails } from '@/components/TransactionDetails/transactionTransformer' import { TRANSACTIONS } from '@/constants/query.consts' @@ -40,6 +40,7 @@ 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' export const TransactionDetailsReceipt = ({ transaction, @@ -1204,6 +1205,23 @@ export const TransactionDetailsReceipt = ({ }} /> )} + +

Contributors (10)

+
+ {Array.from({ length: 10 }).map((_, index) => ( + + ))} +
) } From 7fc1619ed6ea130ff66ade3bee138dba8c145660 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Sun, 12 Oct 2025 20:57:30 +0530 Subject: [PATCH 03/28] feat: create request page changes --- src/assets/icons/index.ts | 1 + src/assets/icons/socials.svg | 34 +++++++++++++++++++ .../Global/PeanutActionCard/index.tsx | 18 +++++++--- src/components/Global/QRCodeWrapper/index.tsx | 2 +- .../Global/TokenAmountInput/index.tsx | 11 ++++++ .../link/views/Create.request.link.view.tsx | 8 ++++- 6 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 src/assets/icons/socials.svg diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index 42b8b0e8e..1db64201d 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -21,3 +21,4 @@ export { default as STAR_STRAIGHT_ICON } from './starStraight.svg' export { default as WHATSAPP_ICON } from './whatsapp.svg' export { default as IMESSAGE_ICON } from './imessage.svg' export { default as FBMessenger_ICON } from './fbmessenger.svg' +export { default as SOCIALS_ICON } from './socials.svg' diff --git a/src/assets/icons/socials.svg b/src/assets/icons/socials.svg new file mode 100644 index 000000000..07e5ed69b --- /dev/null +++ b/src/assets/icons/socials.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Global/PeanutActionCard/index.tsx b/src/components/Global/PeanutActionCard/index.tsx index 237b4a325..db74adff6 100644 --- a/src/components/Global/PeanutActionCard/index.tsx +++ b/src/components/Global/PeanutActionCard/index.tsx @@ -1,5 +1,7 @@ +import Image from 'next/image' import Card from '../Card' import { Icon } from '../Icons/Icon' +import { SOCIALS_ICON } from '@/assets' interface PeanutActionCardProps { type: 'request' | 'send' @@ -7,16 +9,22 @@ interface PeanutActionCardProps { const PeanutActionCard = ({ type }: PeanutActionCardProps) => { return ( - +
-
- {type === 'request' ? 'Request money with a link' : 'Create a payment link'} +
+ {type === 'request' ? 'Request money from friends' : 'Create a payment link'}
-
- {type === 'request' ? `No account needed to pay ` : 'Anyone with the link can receive the money'} +
+ {type === 'request' + ? `They don't need an account to pay` + : 'Anyone with the link can receive the money'} +
+
+ Socials +

Perfect for group chats!

diff --git a/src/components/Global/QRCodeWrapper/index.tsx b/src/components/Global/QRCodeWrapper/index.tsx index a0389cd08..37fad75bf 100644 --- a/src/components/Global/QRCodeWrapper/index.tsx +++ b/src/components/Global/QRCodeWrapper/index.tsx @@ -12,7 +12,7 @@ interface QRCodeWrapperProps { const QRCodeWrapper = ({ url, isLoading = false, disabled = false }: QRCodeWrapperProps) => { return ( -
+
{/* Container with black border and rounded corners */}
{ const { selectedTokenData } = useContext(tokenSelectorContext) const inputRef = useRef(null) @@ -276,6 +281,12 @@ const TokenAmountInput = ({
)} + {showInfoText && infoText && ( +
+ +

{infoText}

+
+ )} ) } 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 a5ab1ef2a..b4475fe10 100644 --- a/src/components/Request/link/views/Create.request.link.view.tsx +++ b/src/components/Request/link/views/Create.request.link.view.tsx @@ -389,6 +389,8 @@ export const CreateRequestLinkView = () => { onSubmit={handleTokenAmountSubmit} walletBalance={peanutWalletBalance} disabled={!!requestId} + showInfoText + infoText="Leave empty to let payers choose amounts." /> {
) : ( - Share Link + + {tokenValue.length === 0 || parseFloat(tokenValue) === 0 + ? 'Share open request' + : `Share $${tokenValue} request`} + )} {errorState.showError && ( From a4e5d77d504cc0ff135ae18aec68fcf269c1e187 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Sun, 12 Oct 2025 22:16:32 +0530 Subject: [PATCH 04/28] feat: txn card changes --- src/components/Global/StatusPill/index.tsx | 12 ++++++------ src/components/Kyc/KycStatusItem.tsx | 2 +- src/components/Profile/AvatarWithBadge.tsx | 6 ------ .../TransactionDetails/TransactionAvatarBadge.tsx | 4 ---- .../TransactionDetails/TransactionCard.tsx | 10 ++++------ 5 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/components/Global/StatusPill/index.tsx b/src/components/Global/StatusPill/index.tsx index a65b597d8..d0271a365 100644 --- a/src/components/Global/StatusPill/index.tsx +++ b/src/components/Global/StatusPill/index.tsx @@ -28,18 +28,18 @@ const StatusPill = ({ status }: StatusPillProps) => { } 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, } return (
diff --git a/src/components/Kyc/KycStatusItem.tsx b/src/components/Kyc/KycStatusItem.tsx index ac4780fd8..1b52ddecc 100644 --- a/src/components/Kyc/KycStatusItem.tsx +++ b/src/components/Kyc/KycStatusItem.tsx @@ -92,7 +92,7 @@ export const KycStatusItem = ({ } export const KYCStatusIcon = () => { - return + return } export const KYCStatusDrawerItem = ({ status }: { status: StatusType }) => { diff --git a/src/components/Profile/AvatarWithBadge.tsx b/src/components/Profile/AvatarWithBadge.tsx index 410249741..046ca79d8 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/TransactionDetails/TransactionAvatarBadge.tsx b/src/components/TransactionDetails/TransactionAvatarBadge.tsx index 86556fe03..b3f3071a3 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 73bf6ffb1..939b7cae7 100644 --- a/src/components/TransactionDetails/TransactionCard.tsx +++ b/src/components/TransactionDetails/TransactionCard.tsx @@ -110,8 +110,6 @@ const TransactionCard: React.FC = ({ width={30} height={30} /> - - {status && }
) : ( = ({ isLinkTransaction={isLinkTx} transactionType={type} context="card" - size="small" - status={status} + size="extra-small" /> )}
@@ -138,9 +135,10 @@ const TransactionCard: React.FC = ({
{/* display the action icon and type text */} -
+
{getActionIcon(type, transaction.direction)} {getActionText(type)} + {status && }
@@ -170,7 +168,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': From 062f946bf552e211ec4350079c77727613d80243 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Mon, 13 Oct 2025 23:29:40 +0530 Subject: [PATCH 05/28] feat: add slider --- src/components/Global/Slider/index.tsx | 87 +++++++++++++++++++ .../Global/TokenAmountInput/index.tsx | 11 ++- src/components/Payment/PaymentForm/index.tsx | 1 + 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/components/Global/Slider/index.tsx diff --git a/src/components/Global/Slider/index.tsx b/src/components/Global/Slider/index.tsx new file mode 100644 index 000000000..01e91c16b --- /dev/null +++ b/src/components/Global/Slider/index.tsx @@ -0,0 +1,87 @@ +'use client' + +import * as React from 'react' +import * as SliderPrimitive from '@radix-ui/react-slider' +import { twMerge } from 'tailwind-merge' + +const PERCENTAGE_OPTIONS = [0, 33, 50, 100, 120] + +function Slider({ + className, + defaultValue = [100], + value: controlledValue, + onValueChange, + ...props +}: React.ComponentProps) { + // 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]) + + const _values = React.useMemo(() => internalValue, [internalValue]) + + // Snap to nearest percentage option during drag + const handleValueChange = React.useCallback( + (newValue: number[]) => { + const snappedValue = PERCENTAGE_OPTIONS.reduce((prev, curr) => + Math.abs(curr - newValue[0]) < Math.abs(prev - newValue[0]) ? curr : prev + ) + const snappedArray = [snappedValue] + setInternalValue(snappedArray) + onValueChange?.(snappedArray) + }, + [onValueChange] + ) + + return ( +
+
+

0%

+

120%

+
+ + + + + {Array.from({ length: _values.length }, (_, index) => ( + +
+ {internalValue[index]}% +
+
+ ))} +
+
+ ) +} + +export { Slider } diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx index b2a778bb8..c00f2ec95 100644 --- a/src/components/Global/TokenAmountInput/index.tsx +++ b/src/components/Global/TokenAmountInput/index.tsx @@ -5,6 +5,7 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'r 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 @@ -25,6 +26,7 @@ interface TokenAmountInputProps { hideBalance?: boolean showInfoText?: boolean infoText?: string + showSlider?: boolean } const TokenAmountInput = ({ @@ -42,6 +44,7 @@ const TokenAmountInput = ({ hideBalance = false, infoText, showInfoText, + showSlider = false, }: TokenAmountInputProps) => { const { selectedTokenData } = useContext(tokenSelectorContext) const inputRef = useRef(null) @@ -206,7 +209,7 @@ const TokenAmountInput = ({ return (
@@ -257,7 +260,6 @@ const TokenAmountInput = ({
)}
- {/* Conversion toggle */} {showConversion && (
{infoText}

)} + {showSlider && ( +
+ +
+ )} ) } diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index 2f3d8d0eb..90e20a994 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -656,6 +656,7 @@ export const PaymentForm = ({ currency={currency} hideCurrencyToggle={!currency} hideBalance={isExternalWalletFlow} + showSlider={flow === 'request_pay'} /> {/* From 0cb620c530a944067344ab3f70c589306076e301 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Tue, 14 Oct 2025 16:11:42 +0530 Subject: [PATCH 06/28] feat: add initial requ fulfilment screen --- src/app/[...recipient]/client.tsx | 11 ++- src/components/Global/Slider/index.tsx | 18 +++-- .../Global/TokenAmountInput/index.tsx | 21 ++++- src/components/Payment/PaymentForm/index.tsx | 1 - .../RequestFulfillmentFlow.tsx | 67 ++++++++++++++++ src/components/User/UserCard.tsx | 79 ++++++++++++------- 6 files changed, 157 insertions(+), 40 deletions(-) create mode 100644 src/components/Payment/Views/RequestFulfillmentViews/RequestFulfillmentFlow.tsx diff --git a/src/app/[...recipient]/client.tsx b/src/app/[...recipient]/client.tsx index 0e9dfdae2..5d378d05c 100644 --- a/src/app/[...recipient]/client.tsx +++ b/src/app/[...recipient]/client.tsx @@ -33,6 +33,7 @@ import NavHeader from '@/components/Global/NavHeader' import { ReqFulfillBankFlowManager } from '@/components/Request/views/ReqFulfillBankFlowManager' import SupportCTA from '@/components/Global/SupportCTA' import { BankRequestType, useDetermineBankRequestType } from '@/hooks/useDetermineBankRequestType' +import RequestFulfillmentFlow from '@/components/Payment/Views/RequestFulfillmentViews/RequestFulfillmentFlow' export type PaymentFlow = 'request_pay' | 'external_wallet' | 'direct_pay' | 'withdraw' interface Props { @@ -447,6 +448,10 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props) return } + if (flow === 'request_pay') { + return + } + // render PUBLIC_PROFILE view if ( currentView === 'PUBLIC_PROFILE' && @@ -501,9 +506,9 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props) flow="request" requestLinkData={parsedPaymentData as ParsedURL} isLoggedIn={!!user?.user.userId} - isInviteLink={ - flow === 'request_pay' && parsedPaymentData?.recipient?.recipientType === 'USERNAME' - } // invite link is only available for request pay flow + // isInviteLink={ + // flow === 'request_pay' && parsedPaymentData?.recipient?.recipientType === 'USERNAME' + // } // invite link is only available for request pay flow /> )}
diff --git a/src/components/Global/Slider/index.tsx b/src/components/Global/Slider/index.tsx index 01e91c16b..241f91370 100644 --- a/src/components/Global/Slider/index.tsx +++ b/src/components/Global/Slider/index.tsx @@ -4,7 +4,7 @@ import * as React from 'react' import * as SliderPrimitive from '@radix-ui/react-slider' import { twMerge } from 'tailwind-merge' -const PERCENTAGE_OPTIONS = [0, 33, 50, 100, 120] +const PERCENTAGE_OPTIONS = [25, 33, 50, 100, 120] function Slider({ className, @@ -32,10 +32,14 @@ function Slider({ Math.abs(curr - newValue[0]) < Math.abs(prev - newValue[0]) ? curr : prev ) const snappedArray = [snappedValue] - setInternalValue(snappedArray) - onValueChange?.(snappedArray) + + // Only update if the value actually changed + if (internalValue[0] !== snappedValue) { + setInternalValue(snappedArray) + onValueChange?.(snappedArray) + } }, - [onValueChange] + [onValueChange, internalValue] ) return ( @@ -75,7 +79,11 @@ function Slider({ )} >
- {internalValue[index]}% + {/* Show decimals only if there are any */} + {internalValue[index] % 1 === 0 + ? internalValue[index].toFixed(0) + : internalValue[index].toFixed(2)} + %
))} diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx index c00f2ec95..81056ae97 100644 --- a/src/components/Global/TokenAmountInput/index.tsx +++ b/src/components/Global/TokenAmountInput/index.tsx @@ -27,6 +27,7 @@ interface TokenAmountInputProps { showInfoText?: boolean infoText?: string showSlider?: boolean + maxAmount?: number } const TokenAmountInput = ({ @@ -45,6 +46,7 @@ const TokenAmountInput = ({ infoText, showInfoText, showSlider = false, + maxAmount, }: TokenAmountInputProps) => { const { selectedTokenData } = useContext(tokenSelectorContext) const inputRef = useRef(null) @@ -134,6 +136,17 @@ const TokenAmountInput = ({ [displayMode, currency?.price, selectedTokenData?.price, calculateAlternativeValue] ) + const onSliderValueChange = useCallback( + (value: number[]) => { + if (maxAmount) { + const selectedPercentage = value[0] + const selectedAmount = (selectedPercentage / 100) * maxAmount + onChange(selectedAmount.toString(), true) + } + }, + [maxAmount, onChange] + ) + const showConversion = useMemo(() => { return !hideCurrencyToggle && (displayMode === 'TOKEN' || displayMode === 'FIAT') }, [hideCurrencyToggle, displayMode]) @@ -225,6 +238,7 @@ const TokenAmountInput = ({ const value = formatAmountWithoutComma(e.target.value) onChange(value, isInputUsd) }} + autoFocus ref={inputRef} inputMode="decimal" type={inputType} @@ -289,9 +303,12 @@ const TokenAmountInput = ({

{infoText}

)} - {showSlider && ( + {showSlider && maxAmount && (
- +
)} diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index 90e20a994..2f3d8d0eb 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -656,7 +656,6 @@ export const PaymentForm = ({ currency={currency} hideCurrencyToggle={!currency} hideBalance={isExternalWalletFlow} - showSlider={flow === 'request_pay'} /> {/* diff --git a/src/components/Payment/Views/RequestFulfillmentViews/RequestFulfillmentFlow.tsx b/src/components/Payment/Views/RequestFulfillmentViews/RequestFulfillmentFlow.tsx new file mode 100644 index 000000000..681bd139f --- /dev/null +++ b/src/components/Payment/Views/RequestFulfillmentViews/RequestFulfillmentFlow.tsx @@ -0,0 +1,67 @@ +'use client' + +import { Button } from '@/components/0_Bruddle' +import NavHeader from '@/components/Global/NavHeader' +import TokenAmountInput from '@/components/Global/TokenAmountInput' +import UserCard from '@/components/User/UserCard' +import React, { useState } from 'react' + +const RequestFulfillmentFlow = () => { + const [inputTokenAmount, setInputTokenAmount] = useState('') + const [inputUsdValue, setInputUsdValue] = useState('') + const [currencyAmount, setCurrencyAmount] = useState('') + const requestAmount = 6969 + const amountCollected = 3479 + + return ( +
+ {}} title="Pay" /> + +
+
+ + + setInputTokenAmount(value || '')} + setUsdValue={(value: string) => { + setInputUsdValue(value) + }} + setCurrencyAmount={(value) => setCurrencyAmount(value ?? '')} + className="w-full" + walletBalance={'10'} + currency={{ code: 'USD', symbol: '$', price: 1 }} + hideCurrencyToggle={true} + hideBalance={false} + showSlider={true} + maxAmount={requestAmount} + /> + + +

Contributors (0)

+
+
+
+ ) +} + +export default RequestFulfillmentFlow diff --git a/src/components/User/UserCard.tsx b/src/components/User/UserCard.tsx index 9b0216a54..b9798af95 100644 --- a/src/components/User/UserCard.tsx +++ b/src/components/User/UserCard.tsx @@ -7,9 +7,11 @@ import Card from '../Global/Card' import { Icon, IconName } from '../Global/Icons/Icon' import AvatarWithBadge, { 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,8 @@ interface UserCardProps { fileUrl?: string isVerified?: boolean haveSentMoneyToUser?: boolean + amount?: number + amountCollected?: number } const UserCard = ({ @@ -30,6 +34,8 @@ const UserCard = ({ fileUrl, isVerified, haveSentMoneyToUser, + amount, + amountCollected, }: UserCardProps) => { const getIcon = (): IconName | undefined => { if (type === 'send') return 'arrow-up-right' @@ -43,6 +49,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 (
@@ -52,35 +59,49 @@ const UserCard = ({ }, [type]) return ( - - -
- {getTitle()} - {recipientType !== 'USERNAME' ? ( - - ) : ( - - )} - + +
+ +
+ {getTitle()} + {recipientType !== 'USERNAME' || type === 'request_pay' ? ( + + ) : ( + + )} + +
+ {amount && amountCollected && type === 'request_pay' && ( + = amount} /> + )}
) } From 15bc994ca946b2d8377bf6f2f425f68cde25ed06 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Tue, 14 Oct 2025 16:34:05 +0530 Subject: [PATCH 07/28] feat: update slider component --- src/components/Global/Slider/index.tsx | 71 +++++++++++-------- .../Global/TokenAmountInput/index.tsx | 4 +- 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/src/components/Global/Slider/index.tsx b/src/components/Global/Slider/index.tsx index 241f91370..48346a528 100644 --- a/src/components/Global/Slider/index.tsx +++ b/src/components/Global/Slider/index.tsx @@ -4,7 +4,8 @@ import * as React from 'react' import * as SliderPrimitive from '@radix-ui/react-slider' import { twMerge } from 'tailwind-merge' -const PERCENTAGE_OPTIONS = [25, 33, 50, 100, 120] +const SNAP_POINTS = [25, 33, 50, 100] +const SNAP_THRESHOLD = 5 // ±5% proximity to trigger snap function Slider({ className, @@ -23,20 +24,31 @@ function Slider({ } }, [controlledValue]) - const _values = React.useMemo(() => internalValue, [internalValue]) + // 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]) - // Snap to nearest percentage option during drag + // Soft snap to nearby snap points with ±5% threshold const handleValueChange = React.useCallback( (newValue: number[]) => { - const snappedValue = PERCENTAGE_OPTIONS.reduce((prev, curr) => - Math.abs(curr - newValue[0]) < Math.abs(prev - newValue[0]) ? curr : prev - ) - const snappedArray = [snappedValue] + 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] !== snappedValue) { - setInternalValue(snappedArray) - onValueChange?.(snappedArray) + if (internalValue[0] !== finalValue) { + setInternalValue(finalArray) + onValueChange?.(finalArray) } }, [onValueChange, internalValue] @@ -67,26 +79,29 @@ function Slider({ > - {Array.from({ length: _values.length }, (_, index) => ( - -
- {/* Show decimals only if there are any */} - {internalValue[index] % 1 === 0 - ? internalValue[index].toFixed(0) - : internalValue[index].toFixed(2)} - % -
-
- ))} + + + {/* 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)}% +
+
) diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx index 81056ae97..4a031c21e 100644 --- a/src/components/Global/TokenAmountInput/index.tsx +++ b/src/components/Global/TokenAmountInput/index.tsx @@ -140,8 +140,8 @@ const TokenAmountInput = ({ (value: number[]) => { if (maxAmount) { const selectedPercentage = value[0] - const selectedAmount = (selectedPercentage / 100) * maxAmount - onChange(selectedAmount.toString(), true) + const selectedAmount = parseFloat(((selectedPercentage / 100) * maxAmount).toFixed(4)).toString() + onChange(selectedAmount, true) } }, [maxAmount, onChange] From 74d64ef09b688ab33d42dc3ef8bd5444e82db6e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Wed, 15 Oct 2025 18:07:02 -0300 Subject: [PATCH 08/28] refactor: change request api to use jwt token --- src/services/requests.ts | 45 ++++++++++++++++++---------------- src/services/services.types.ts | 3 --- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/services/requests.ts b/src/services/requests.ts index b0f80c45c..4a1ec30a3 100644 --- a/src/services/requests.ts +++ b/src/services/requests.ts @@ -1,20 +1,17 @@ import { PEANUT_API_URL } from '@/constants' import { CreateRequestRequest, 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 response = await fetchWithSentry(`${PEANUT_API_URL}/requests`, { method: 'POST', - body: formData, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${Cookies.get('jwt-token')}`, + }, + body: jsonStringify(data), }) if (!response.ok) { @@ -37,17 +34,13 @@ 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 response = await fetchWithSentry(`${PEANUT_API_URL}/requests/${id}`, { method: 'PATCH', - body: formData, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${Cookies.get('jwt-token')}`, + }, + body: jsonStringify(data), }) if (!response.ok) { @@ -85,4 +78,14 @@ export const requestsApi = { } return response.json() }, + + close: async (uuid: string): Promise => { + const response = await fetchWithSentry(`${PEANUT_API_URL}/requests/${uuid}`, { + method: 'DELETE', + }) + 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 1f5180658..43cfd67b4 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 { From 3aff7883ee14ba597ea41a22cd3b14bd3dd3ba8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= <70615692+jjramirezn@users.noreply.github.com> Date: Wed, 15 Oct 2025 18:24:34 -0300 Subject: [PATCH 09/28] Update src/services/requests.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Juan José Ramírez <70615692+jjramirezn@users.noreply.github.com> --- src/services/requests.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/services/requests.ts b/src/services/requests.ts index 4a1ec30a3..1816cd54c 100644 --- a/src/services/requests.ts +++ b/src/services/requests.ts @@ -80,8 +80,15 @@ export const requestsApi = { }, 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}`) From 06625ff47d1b479a8375c9167a0920a43f2b1618 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Thu, 16 Oct 2025 17:33:42 +0530 Subject: [PATCH 10/28] feat: remove direct request --- src/app/(mobile-ui)/request/page.tsx | 4 ++-- .../link/views/Create.request.link.view.tsx | 2 +- .../Request/views/RequestRouter.view.tsx | 16 ---------------- 3 files changed, 3 insertions(+), 19 deletions(-) delete mode 100644 src/components/Request/views/RequestRouter.view.tsx diff --git a/src/app/(mobile-ui)/request/page.tsx b/src/app/(mobile-ui)/request/page.tsx index 2276df456..d870334c9 100644 --- a/src/app/(mobile-ui)/request/page.tsx +++ b/src/app/(mobile-ui)/request/page.tsx @@ -1,6 +1,6 @@ import { generateMetadata } from '@/app/metadata' import PageContainer from '@/components/0_Bruddle/PageContainer' -import { RequestRouterView } from '@/components/Request/views/RequestRouter.view' +import { CreateRequestLinkView } from '@/components/Request/link/views/Create.request.link.view' export const metadata = generateMetadata({ title: 'Request Money | Peanut', @@ -13,7 +13,7 @@ export const metadata = generateMetadata({ export default function RequestPage() { return ( - + ) } 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 b4475fe10..1808c86ce 100644 --- a/src/components/Request/link/views/Create.request.link.view.tsx +++ b/src/components/Request/link/views/Create.request.link.view.tsx @@ -376,7 +376,7 @@ export const CreateRequestLinkView = () => { return (
- router.push('/request')} title="Request" /> + router.push('/home')} title="Request" />
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}`)} - /> - ) -} From db3291dc8d122d1fc2c2f03f7ad4771dfdfce56d Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Thu, 16 Oct 2025 18:00:55 +0530 Subject: [PATCH 11/28] feat: update request creation process --- src/app/(mobile-ui)/request/create/page.tsx | 18 ------------------ .../link/views/Create.request.link.view.tsx | 13 ------------- 2 files changed, 31 deletions(-) delete mode 100644 src/app/(mobile-ui)/request/create/page.tsx diff --git a/src/app/(mobile-ui)/request/create/page.tsx b/src/app/(mobile-ui)/request/create/page.tsx deleted file mode 100644 index 6e05bd931..000000000 --- a/src/app/(mobile-ui)/request/create/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { generateMetadata } from '@/app/metadata' -import PageContainer from '@/components/0_Bruddle/PageContainer' -import { CreateRequestLinkView } from '@/components/Request/link/views/Create.request.link.view' - -export const metadata = generateMetadata({ - title: 'Request Payment | Peanut', - description: 'Request cryptocurrency from friends, family, or anyone else using Peanut on any chain.', - image: '/metadata-img.png', - keywords: 'crypto request, crypto payment, crypto invoice, crypto payment link', -}) - -export default function RequestCreate() { - return ( - - - - ) -} 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 1808c86ce..b6708edb0 100644 --- a/src/components/Request/link/views/Create.request.link.view.tsx +++ b/src/components/Request/link/views/Create.request.link.view.tsx @@ -169,21 +169,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 From 9d7331f05cd5d981b85d4181e7d7c2ea95f05f08 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Fri, 17 Oct 2025 18:41:43 +0530 Subject: [PATCH 12/28] feat: update request fulfilment flow to handle request pots --- src/app/[...recipient]/client.tsx | 14 ++-- src/components/Global/Contributors/index.tsx | 24 +++++++ .../Global/TokenAmountInput/index.tsx | 2 +- src/components/Payment/PaymentForm/index.tsx | 61 ++++++++++++++--- .../RequestFulfillmentFlow.tsx | 67 ------------------- src/components/User/UserCard.tsx | 2 +- src/hooks/usePaymentInitiator.ts | 29 ++++---- src/utils/general.utils.ts | 6 ++ 8 files changed, 102 insertions(+), 103 deletions(-) create mode 100644 src/components/Global/Contributors/index.tsx delete mode 100644 src/components/Payment/Views/RequestFulfillmentViews/RequestFulfillmentFlow.tsx diff --git a/src/app/[...recipient]/client.tsx b/src/app/[...recipient]/client.tsx index bc8aeffa5..1ca162e9d 100644 --- a/src/app/[...recipient]/client.tsx +++ b/src/app/[...recipient]/client.tsx @@ -33,7 +33,6 @@ import NavHeader from '@/components/Global/NavHeader' import { ReqFulfillBankFlowManager } from '@/components/Request/views/ReqFulfillBankFlowManager' import SupportCTA from '@/components/Global/SupportCTA' import { BankRequestType, useDetermineBankRequestType } from '@/hooks/useDetermineBankRequestType' -import RequestFulfillmentFlow from '@/components/Payment/Views/RequestFulfillmentViews/RequestFulfillmentFlow' import { useQuery } from '@tanstack/react-query' import { pointsApi } from '@/services/points' import { PointsAction } from '@/services/services.types' @@ -475,10 +474,6 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props) return } - if (flow === 'request_pay') { - return - } - // render PUBLIC_PROFILE view if ( currentView === 'PUBLIC_PROFILE' && @@ -526,16 +521,17 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props) } setCurrencyAmount={(value: string | undefined) => setCurrencyAmount(value || '')} currencyAmount={currencyAmount} + isRequestPotPayment={!!requestId} />
- {showActionList && ( + {chargeId && showActionList && ( )}
diff --git a/src/components/Global/Contributors/index.tsx b/src/components/Global/Contributors/index.tsx new file mode 100644 index 000000000..a80f73a1c --- /dev/null +++ b/src/components/Global/Contributors/index.tsx @@ -0,0 +1,24 @@ +import { ChargeEntry } from '@/services/services.types' +import { getContributorsFromCharge } from '@/utils' +import React from 'react' + +const Contributors = ({ charges }: { charges: ChargeEntry[] }) => { + const contributors = getContributorsFromCharge(charges) + + return ( +
+

Contributors ({contributors.length})

+ {contributors.map((contributor) => ( +
+

+ {contributor.payments[contributor.payments.length - 1]?.payerAccount?.user?.username ?? + 'Anonymous'} +

+

{contributor.tokenAmount}

+
+ ))} +
+ ) +} + +export default Contributors diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx index 4958c796b..fff7c4a56 100644 --- a/src/components/Global/TokenAmountInput/index.tsx +++ b/src/components/Global/TokenAmountInput/index.tsx @@ -141,7 +141,7 @@ const TokenAmountInput = ({ if (maxAmount) { const selectedPercentage = value[0] const selectedAmount = parseFloat(((selectedPercentage / 100) * maxAmount).toFixed(4)).toString() - onChange(selectedAmount, true) + onChange(selectedAmount, isInputUsd) } }, [maxAmount, onChange] diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index 2f3d8d0eb..b9428254d 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -35,6 +35,7 @@ import { PaymentFlow } from '@/app/[...recipient]/client' import MantecaFulfillment from '../Views/MantecaFulfillment.view' import { invitesApi } from '@/services/invites' import { EInviteType } from '@/services/services.types' +import Contributors from '@/components/Global/Contributors' export type PaymentFlowProps = { isExternalWalletFlow?: boolean @@ -49,6 +50,7 @@ export type PaymentFlowProps = { setCurrencyAmount?: (currencyvalue: string | undefined) => void headerTitle?: string flow?: PaymentFlow + isRequestPotPayment?: boolean } export type PaymentFormProps = ParsedURL & PaymentFlowProps @@ -65,6 +67,7 @@ export const PaymentForm = ({ isDirectUsdPayment, headerTitle, flow, + isRequestPotPayment, }: PaymentFormProps) => { const dispatch = useAppDispatch() const router = useRouter() @@ -209,7 +212,11 @@ export const PaymentForm = ({ } } else { // regular send/pay - if (isActivePeanutWallet && areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN)) { + if ( + !isRequestPotPayment && + isActivePeanutWallet && + areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN) + ) { // peanut wallet payment const walletNumeric = parseFloat(String(peanutWalletBalance).replace(/,/g, '')) if (walletNumeric < parsedInputAmount) { @@ -261,6 +268,7 @@ export const PaymentForm = ({ selectedTokenData, isExternalWalletConnected, isExternalWalletFlow, + isRequestPotPayment, ]) // fetch token price @@ -308,6 +316,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 (isRequestPotPayment) { + return recipientExists && amountIsSet && tokenSelected && isRequestPotPayment + } + return recipientExists && amountIsSet && tokenSelected && walletConnected }, [ recipient, @@ -350,7 +363,7 @@ export const PaymentForm = ({ if (inviteError) { setInviteError(false) } - if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) { + if (!isRequestPotPayment && 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() @@ -360,16 +373,17 @@ export const PaymentForm = ({ return } - if (!isExternalWalletConnected && isExternalWalletFlow) { + if (!isRequestPotPayment && !isExternalWalletConnected && isExternalWalletFlow) { openReownModal() return } - if (!isConnected) { + if (!isRequestPotPayment && !isConnected) { dispatch(walletActions.setSignInModalVisible(true)) return } + console.log('hello') if (!canInitiatePayment) return // regular payment flow @@ -422,8 +436,16 @@ export const PaymentForm = ({ ? 'DIRECT_SEND' : 'REQUEST', attachmentOptions: attachmentOptions, + isRequestPotPayment, } + // if (isRequestPotPayment) { + // const res = await createRequestCharge(payload) + // console.log(res) + // dispatch(paymentActions.setView('CONFIRM')) + // return + // } + console.log('Initiating payment with payload:', payload) const result = await initiatePayment(payload) @@ -431,7 +453,7 @@ export const PaymentForm = ({ if (result.status === 'Success') { dispatch(paymentActions.setView('STATUS')) } else if (result.status === 'Charge Created') { - if (!fulfillUsingManteca) { + if (!fulfillUsingManteca && !isRequestPotPayment) { dispatch(paymentActions.setView('CONFIRM')) } } else if (result.status === 'Error') { @@ -473,6 +495,10 @@ export const PaymentForm = ({ return 'Send' } + if (isRequestPotPayment) { + return 'Pay' + } + if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) { return (
@@ -501,11 +527,13 @@ export const PaymentForm = ({ } const getButtonIcon = (): IconName | undefined => { - if (!isExternalWalletConnected && isExternalWalletFlow) return 'wallet-outline' + if (!isRequestPotPayment && !isExternalWalletConnected && isExternalWalletFlow) return 'wallet-outline' - if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) return 'arrow-down' + if (!isRequestPotPayment && isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) + return 'arrow-down' - if (!isProcessing && isActivePeanutWallet && !isExternalWalletFlow) return 'arrow-up-right' + if (!isRequestPotPayment && !isProcessing && isActivePeanutWallet && !isExternalWalletFlow) + return 'arrow-up-right' return undefined } @@ -630,7 +658,7 @@ export const PaymentForm = ({ {/* Recipient Info Card */} {recipient && !isExternalWalletFlow && ( )} @@ -651,11 +681,17 @@ export const PaymentForm = ({ }} setCurrencyAmount={setCurrencyAmount} className="w-full" - disabled={!isExternalWalletFlow && (!!requestDetails?.tokenAmount || !!chargeDetails?.tokenAmount)} + disabled={ + !isRequestPotPayment && + !isExternalWalletFlow && + (!!requestDetails?.tokenAmount || !!chargeDetails?.tokenAmount) + } walletBalance={isActivePeanutWallet ? peanutWalletBalance : undefined} currency={currency} hideCurrencyToggle={!currency} hideBalance={isExternalWalletFlow} + showSlider={isRequestPotPayment} + maxAmount={isRequestPotPayment ? Number(amount) : undefined} /> {/* @@ -693,7 +729,7 @@ export const PaymentForm = ({ )}
- {isPeanutWalletConnected && (!error || isInsufficientBalanceError) && ( + {(isRequestPotPayment || (isPeanutWalletConnected && (!error || isInsufficientBalanceError))) && (
+ + {isRequestPotPayment && } + setDisconnectWagmiModal(false)} diff --git a/src/components/Payment/Views/RequestFulfillmentViews/RequestFulfillmentFlow.tsx b/src/components/Payment/Views/RequestFulfillmentViews/RequestFulfillmentFlow.tsx deleted file mode 100644 index 681bd139f..000000000 --- a/src/components/Payment/Views/RequestFulfillmentViews/RequestFulfillmentFlow.tsx +++ /dev/null @@ -1,67 +0,0 @@ -'use client' - -import { Button } from '@/components/0_Bruddle' -import NavHeader from '@/components/Global/NavHeader' -import TokenAmountInput from '@/components/Global/TokenAmountInput' -import UserCard from '@/components/User/UserCard' -import React, { useState } from 'react' - -const RequestFulfillmentFlow = () => { - const [inputTokenAmount, setInputTokenAmount] = useState('') - const [inputUsdValue, setInputUsdValue] = useState('') - const [currencyAmount, setCurrencyAmount] = useState('') - const requestAmount = 6969 - const amountCollected = 3479 - - return ( -
- {}} title="Pay" /> - -
-
- - - setInputTokenAmount(value || '')} - setUsdValue={(value: string) => { - setInputUsdValue(value) - }} - setCurrencyAmount={(value) => setCurrencyAmount(value ?? '')} - className="w-full" - walletBalance={'10'} - currency={{ code: 'USD', symbol: '$', price: 1 }} - hideCurrencyToggle={true} - hideBalance={false} - showSlider={true} - maxAmount={requestAmount} - /> - - -

Contributors (0)

-
-
-
- ) -} - -export default RequestFulfillmentFlow diff --git a/src/components/User/UserCard.tsx b/src/components/User/UserCard.tsx index b9798af95..0876b8a76 100644 --- a/src/components/User/UserCard.tsx +++ b/src/components/User/UserCard.tsx @@ -99,7 +99,7 @@ const UserCard = ({
- {amount && amountCollected && type === 'request_pay' && ( + {amount !== undefined && amountCollected !== undefined && type === 'request_pay' && ( = amount} /> )} diff --git a/src/hooks/usePaymentInitiator.ts b/src/hooks/usePaymentInitiator.ts index d7cd7e8d2..986e2d7ab 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 + isRequestPotPayment?: 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,7 @@ 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()) + router.replace(newUrl.pathname + newUrl.search) } } return { @@ -168,7 +171,7 @@ export const usePaymentInitiator = () => { success: false, } }, - [activeChargeDetails] + [activeChargeDetails, router] ) // prepare transaction details (called from Confirm view) @@ -391,11 +394,7 @@ 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 - ) + router.replace(newUrl.pathname + newUrl.search) console.log('Updated URL with chargeId:', newUrl.href) } } @@ -619,12 +618,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.isRequestPotPayment || // 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 +673,7 @@ 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()) + router.replace(newUrl.pathname + newUrl.search) console.log('URL updated, chargeId removed.') } } @@ -691,6 +691,7 @@ export const usePaymentInitiator = () => { handleError, setLoadingStep, setError, + router, setTransactionHash, setPaymentDetails, loadingStep, diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts index 79a411b48..5dfc1c4d4 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 { ChargeEntry } from '@/services/services.types' export function urlBase64ToUint8Array(base64String: string) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4) @@ -1356,3 +1357,8 @@ export const getValidRedirectUrl = (redirectUrl: string, fallbackRoute: string) return fallbackRoute } } + +export const getContributorsFromCharge = (charges: ChargeEntry[]) => { + // Contributors are the users who have paid for the charge + return charges.filter((charge) => charge.fulfillmentPayment) +} From ea5c11f5c9a651c3ff2cc6c109faf951c03e125c Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Fri, 17 Oct 2025 20:00:59 +0530 Subject: [PATCH 13/28] feat: receipt changes --- .../Global/Contributors/ContributorCard.tsx | 44 +++++++++++++++ src/components/Global/Contributors/index.tsx | 16 +++--- .../TransactionDetailsHeaderCard.tsx | 14 +++-- .../TransactionDetailsReceipt.tsx | 53 +++++++++++++------ .../transactionTransformer.ts | 6 ++- src/utils/general.utils.ts | 12 ++++- src/utils/history.utils.ts | 3 ++ 7 files changed, 118 insertions(+), 30 deletions(-) create mode 100644 src/components/Global/Contributors/ContributorCard.tsx diff --git a/src/components/Global/Contributors/ContributorCard.tsx b/src/components/Global/Contributors/ContributorCard.tsx new file mode 100644 index 000000000..89d0d0b70 --- /dev/null +++ b/src/components/Global/Contributors/ContributorCard.tsx @@ -0,0 +1,44 @@ +import { ChargeEntry, Payment } from '@/services/services.types' +import Card, { CardPosition } from '../Card' +import AvatarWithBadge from '@/components/Profile/AvatarWithBadge' +import { getColorForUsername } from '@/utils/color.utils' +import { VerifiedUserLabel } from '@/components/UserHeader' +import { formatTokenAmount } from '@/utils' + +export type Contributor = { + uuid: string + payments: Payment[] + amount: string + username: string | undefined + fulfilmentPayment: Payment | null + isUserVerified: boolean +} + +const ContributorCard = ({ contributor, position }: { contributor: Contributor; position: CardPosition }) => { + const colors = getColorForUsername(contributor.username ?? '') + return ( + +
+
+ + + +
+ +

{formatTokenAmount(Number(contributor.amount))}

+
+
+ ) +} + +export default ContributorCard diff --git a/src/components/Global/Contributors/index.tsx b/src/components/Global/Contributors/index.tsx index a80f73a1c..d2641bb78 100644 --- a/src/components/Global/Contributors/index.tsx +++ b/src/components/Global/Contributors/index.tsx @@ -1,6 +1,8 @@ import { ChargeEntry } from '@/services/services.types' import { getContributorsFromCharge } from '@/utils' import React from 'react' +import ContributorCard from './ContributorCard' +import { getCardPosition } from '../Card' const Contributors = ({ charges }: { charges: ChargeEntry[] }) => { const contributors = getContributorsFromCharge(charges) @@ -8,14 +10,12 @@ const Contributors = ({ charges }: { charges: ChargeEntry[] }) => { return (

Contributors ({contributors.length})

- {contributors.map((contributor) => ( -
-

- {contributor.payments[contributor.payments.length - 1]?.payerAccount?.user?.username ?? - 'Anonymous'} -

-

{contributor.tokenAmount}

-
+ {contributors.map((contributor, index) => ( + ))}
) diff --git a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx index fa5e3410f..29286033c 100644 --- a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx +++ b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx @@ -41,6 +41,9 @@ interface TransactionDetailsHeaderCardProps { haveSentMoneyToUser?: boolean hasPerk?: boolean isAvatarClickable?: boolean + showProgessBar?: boolean + progress?: number + goal?: number } const getTitle = ( @@ -173,6 +176,9 @@ export const TransactionDetailsHeaderCard: React.FC { const router = useRouter() const typeForAvatar = @@ -236,9 +242,11 @@ export const TransactionDetailsHeaderCard: React.FC
-
- -
+ {showProgessBar && goal !== undefined && progress !== undefined && ( +
+ +
+ )} ) } diff --git a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx index aab27c758..ddef49c13 100644 --- a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx +++ b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx @@ -10,7 +10,14 @@ import { useUserStore } from '@/redux/hooks' import { chargesApi } from '@/services/charges' import { sendLinksApi } from '@/services/sendLinks' 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' @@ -42,6 +49,7 @@ 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' export const TransactionDetailsReceipt = ({ transaction, @@ -236,6 +244,16 @@ export const TransactionDetailsReceipt = ({ return false }, [transaction, isPendingSentLink, isPendingRequester, isPendingRequestee]) + const requestPotContributors = useMemo(() => { + if (!transaction || !transaction.requestPotPayments) return [] + return getContributorsFromCharge(transaction.requestPotPayments) + }, [transaction]) + + const totalAmountCollected = useMemo(() => { + if (!transaction || !transaction.requestPotPayments) return 0 + return requestPotContributors.reduce((acc, curr) => acc + Number(curr.amount), 0) + }, [requestPotContributors]) + useEffect(() => { const getTokenDetails = async () => { if (!transaction) { @@ -370,6 +388,9 @@ export const TransactionDetailsReceipt = ({ haveSentMoneyToUser={transaction.haveSentMoneyToUser} hasPerk={!!transaction.extraDataForDrawer?.perk?.claimed} isAvatarClickable={isAvatarClickable} + showProgessBar={transaction.isRequestPotLink} + goal={Number(transaction.amount)} + progress={totalAmountCollected} /> {/* details card (date, fee, memo) and more */} @@ -1207,22 +1228,20 @@ export const TransactionDetailsReceipt = ({ /> )} -

Contributors (10)

-
- {Array.from({ length: 10 }).map((_, index) => ( - - ))} -
+ {requestPotContributors.length > 0 && ( + <> +

Contributors ({requestPotContributors.length})

+
+ {requestPotContributors.map((contributor, index) => ( + + ))} +
+ + )}
) } diff --git a/src/components/TransactionDetails/transactionTransformer.ts b/src/components/TransactionDetails/transactionTransformer.ts index 4b66aa2bb..cd7b0e333 100644 --- a/src/components/TransactionDetails/transactionTransformer.ts +++ b/src/components/TransactionDetails/transactionTransformer.ts @@ -13,7 +13,7 @@ import { import { StatusPillType } from '../Global/StatusPill' import type { Address } from 'viem' import { PEANUT_WALLET_CHAIN } from '@/constants' -import { HistoryEntryPerk } from '@/services/services.types' +import { ChargeEntry, HistoryEntryPerk } from '@/services/services.types' /** * @fileoverview maps raw transaction history data from the api/hook to the format needed by ui components. @@ -121,6 +121,8 @@ export interface TransactionDetails { createdAt?: string | Date completedAt?: string | Date points?: number + isRequestPotLink?: boolean + requestPotPayments?: ChargeEntry[] } /** @@ -514,6 +516,8 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact createdAt: entry.createdAt, completedAt: entry.completedAt, haveSentMoneyToUser: entry.extraData?.haveSentMoneyToUser as boolean, + isRequestPotLink: entry.isRequestLink, + requestPotPayments: entry.charges, } return { diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts index 5dfc1c4d4..0d3c104be 100644 --- a/src/utils/general.utils.ts +++ b/src/utils/general.utils.ts @@ -1360,5 +1360,15 @@ export const getValidRedirectUrl = (redirectUrl: string, fallbackRoute: string) export const getContributorsFromCharge = (charges: ChargeEntry[]) => { // Contributors are the users who have paid for the charge - return charges.filter((charge) => charge.fulfillmentPayment) + return charges + .filter((charge) => charge.fulfillmentPayment) + .map((charge) => ({ + uuid: charge.uuid, + payments: charge.payments, + amount: charge.tokenAmount, + username: charge.payments[charge.payments.length - 1].payerAccount?.user?.username, + fulfilmentPayment: charge.fulfillmentPayment, + isUserVerified: + charge.payments[charge.payments.length - 1].payerAccount?.user?.bridgeKycStatus === 'approved', + })) } diff --git a/src/utils/history.utils.ts b/src/utils/history.utils.ts index 7f710ba0b..48c12b587 100644 --- a/src/utils/history.utils.ts +++ b/src/utils/history.utils.ts @@ -6,6 +6,7 @@ import { formatUnits } from 'viem' import { Hash } from 'viem' import { getTokenDetails } from '@/utils' import { getCurrencyPrice } from '@/app/actions/currency' +import { ChargeEntry } from '@/services/services.types' export enum EHistoryEntryType { REQUEST = 'REQUEST', @@ -128,6 +129,8 @@ export type HistoryEntry = { completedAt?: string | Date isVerified?: boolean points?: number + isRequestLink?: boolean // true if the transaction is a request pot link + charges?: ChargeEntry[] } export function isFinalState(transaction: Pick): boolean { From e7a50c7a4d6c4d27fdfd8814fdda09682af272ba Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Sat, 18 Oct 2025 12:37:15 +0530 Subject: [PATCH 14/28] feat: changes required for open requests --- src/components/Global/ProgressBar/index.tsx | 2 +- src/components/Payment/PaymentForm/index.tsx | 30 +++++++++++++++---- .../link/views/Create.request.link.view.tsx | 10 ------- .../TransactionDetailsHeaderCard.tsx | 15 ++++++++-- .../TransactionDetailsReceipt.tsx | 9 +++++- src/components/User/UserCard.tsx | 16 ++++++++-- src/utils/history.utils.ts | 9 +++++- 7 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/components/Global/ProgressBar/index.tsx b/src/components/Global/ProgressBar/index.tsx index 668fc8e6f..16561dd6f 100644 --- a/src/components/Global/ProgressBar/index.tsx +++ b/src/components/Global/ProgressBar/index.tsx @@ -43,7 +43,7 @@ const ProgressBar: React.FC = ({ goal, progress, isClosed }) = return (

{formatCurrency(progress)} contributed

-

{formatCurrency(goal - progress)} remaining

+

{formatCurrency(Math.max(goal - progress, 0))} remaining

) } diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index b9428254d..c74d860a0 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -22,7 +22,7 @@ import { 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,7 +35,8 @@ import { PaymentFlow } from '@/app/[...recipient]/client' import MantecaFulfillment from '../Views/MantecaFulfillment.view' import { invitesApi } from '@/services/invites' import { EInviteType } from '@/services/services.types' -import Contributors from '@/components/Global/Contributors' +import ContributorCard from '@/components/Global/Contributors/ContributorCard' +import { getCardPosition } from '@/components/Global/Card' export type PaymentFlowProps = { isExternalWalletFlow?: boolean @@ -626,6 +627,13 @@ export const PaymentForm = ({ } } + const contributors = getContributorsFromCharge(requestDetails?.charges || []) + + const totalAmountCollected = useMemo(() => { + if (!requestDetails?.charges || !requestDetails?.charges.length) return 0 + return contributors.reduce((acc, curr) => acc + Number(curr.amount), 0) + }, [contributors]) + if (fulfillUsingManteca && chargeDetails) { return } @@ -667,7 +675,8 @@ export const PaymentForm = ({ isVerified={recipientKycStatus === 'approved'} haveSentMoneyToUser={recipientUserId ? interactions[recipientUserId] || false : false} amount={isRequestPotPayment ? Number(amount) : undefined} - amountCollected={isRequestPotPayment ? 0 : undefined} // TODO + amountCollected={isRequestPotPayment ? totalAmountCollected : undefined} // TODO + isRequestPot={isRequestPotPayment} /> )} @@ -690,7 +699,7 @@ export const PaymentForm = ({ currency={currency} hideCurrencyToggle={!currency} hideBalance={isExternalWalletFlow} - showSlider={isRequestPotPayment} + showSlider={isRequestPotPayment && Number(amount) > 0} maxAmount={isRequestPotPayment ? Number(amount) : undefined} /> @@ -773,7 +782,18 @@ export const PaymentForm = ({
- {isRequestPotPayment && } + {isRequestPotPayment && ( +
+

Contributors ({contributors.length})

+ {contributors.map((contributor, index) => ( + + ))} +
+ )} { }) 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() @@ -342,7 +333,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 diff --git a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx index 29286033c..fa99ed700 100644 --- a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx +++ b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx @@ -44,6 +44,7 @@ interface TransactionDetailsHeaderCardProps { showProgessBar?: boolean progress?: number goal?: number + isRequestPotTransaction?: boolean } const getTitle = ( @@ -179,6 +180,7 @@ export const TransactionDetailsHeaderCard: React.FC { const router = useRouter() const typeForAvatar = @@ -192,6 +194,8 @@ export const TransactionDetailsHeaderCard: React.FC
@@ -236,13 +240,20 @@ export const TransactionDetailsHeaderCard: React.FC

{amountDisplay}

+ + {isNoGoalSet &&

No goal set

}
- {showProgessBar && goal !== undefined && progress !== undefined && ( + {!isNoGoalSet && showProgessBar && goal !== undefined && progress !== undefined && (
diff --git a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx index ddef49c13..a65c231e0 100644 --- a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx +++ b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx @@ -316,7 +316,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' @@ -340,6 +340,12 @@ export const TransactionDetailsReceipt = ({ } } + console.log(transaction) + + if (transaction.isRequestPotLink && Number(transaction.amount) === 0) { + amountDisplay = `$${formatCurrency(totalAmountCollected.toString())} 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 && @@ -391,6 +397,7 @@ export const TransactionDetailsReceipt = ({ showProgessBar={transaction.isRequestPotLink} goal={Number(transaction.amount)} progress={totalAmountCollected} + isRequestPotTransaction={transaction.isRequestPotLink} /> {/* details card (date, fee, memo) and more */} diff --git a/src/components/User/UserCard.tsx b/src/components/User/UserCard.tsx index 0876b8a76..1ce87abe8 100644 --- a/src/components/User/UserCard.tsx +++ b/src/components/User/UserCard.tsx @@ -22,6 +22,7 @@ interface UserCardProps { haveSentMoneyToUser?: boolean amount?: number amountCollected?: number + isRequestPot?: boolean } const UserCard = ({ @@ -36,6 +37,7 @@ const UserCard = ({ haveSentMoneyToUser, amount, amountCollected, + isRequestPot, }: UserCardProps) => { const getIcon = (): IconName | undefined => { if (type === 'send') return 'arrow-up-right' @@ -58,6 +60,13 @@ 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 (
@@ -80,7 +89,8 @@ const UserCard = ({ {getTitle()} {recipientType !== 'USERNAME' || type === 'request_pay' ? (
- {amount !== undefined && amountCollected !== undefined && type === 'request_pay' && ( + {amount !== undefined && amountCollected !== undefined && type === 'request_pay' && amount > 0 && ( = amount} /> )}
diff --git a/src/utils/history.utils.ts b/src/utils/history.utils.ts index 48c12b587..57097f7f7 100644 --- a/src/utils/history.utils.ts +++ b/src/utils/history.utils.ts @@ -221,7 +221,14 @@ export async function completeHistoryEntry(entry: HistoryEntry): Promise Date: Sat, 18 Oct 2025 13:11:42 +0530 Subject: [PATCH 15/28] feat: txn receipt changes --- .../TransactionDetails/TransactionCard.tsx | 21 ++++++++++++------- .../TransactionDetailsHeaderCard.tsx | 13 ++++++------ .../TransactionDetailsReceipt.tsx | 17 +++++++-------- .../transactionTransformer.ts | 2 ++ src/utils/general.utils.ts | 4 ++-- src/utils/history.utils.ts | 1 + 6 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/components/TransactionDetails/TransactionCard.tsx b/src/components/TransactionDetails/TransactionCard.tsx index 787ec12b0..853249334 100644 --- a/src/components/TransactionDetails/TransactionCard.tsx +++ b/src/components/TransactionDetails/TransactionCard.tsx @@ -21,6 +21,7 @@ import { VerifiedUserLabel } from '../UserHeader' import { isAddress } from 'viem' import { STAR_STRAIGHT_ICON } from '@/assets' import { HistoryEntryPerk } from '@/services/services.types' +import { twMerge } from 'tailwind-merge' export type TransactionType = | 'send' @@ -92,8 +93,16 @@ const TransactionCard: React.FC = ({ const perkInfo = transaction.extraDataForDrawer?.perk as HistoryEntryPerk | undefined const hasPerk = perkInfo?.claimed - 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') { @@ -158,11 +167,9 @@ const TransactionCard: React.FC = ({ )}
- {hasPerk ? ( - {displayAmount} - ) : ( - {displayAmount} - )} + + {displayAmount} + {currencyDisplayAmount && ( {currencyDisplayAmount} )} diff --git a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx index fa99ed700..8886b594d 100644 --- a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx +++ b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx @@ -217,11 +217,11 @@ export const TransactionDetailsHeaderCard: React.FC )}
-
+

{icon && } - { -
- {status && } -
- } + +
+ {status && } +

{ - if (!transaction || !transaction.requestPotPayments) return 0 - return requestPotContributors.reduce((acc, curr) => acc + Number(curr.amount), 0) - }, [requestPotContributors]) + const formattedTotalAmountCollected = formatCurrency(transaction?.totalAmountCollected.toString() ?? '0', 2, 0) useEffect(() => { const getTokenDetails = async () => { @@ -316,7 +313,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 - let 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' @@ -340,10 +337,10 @@ export const TransactionDetailsReceipt = ({ } } - console.log(transaction) - - if (transaction.isRequestPotLink && Number(transaction.amount) === 0) { - amountDisplay = `$${formatCurrency(totalAmountCollected.toString())} collected` + 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 @@ -396,7 +393,7 @@ export const TransactionDetailsReceipt = ({ isAvatarClickable={isAvatarClickable} showProgessBar={transaction.isRequestPotLink} goal={Number(transaction.amount)} - progress={totalAmountCollected} + progress={Number(formattedTotalAmountCollected)} isRequestPotTransaction={transaction.isRequestPotLink} /> diff --git a/src/components/TransactionDetails/transactionTransformer.ts b/src/components/TransactionDetails/transactionTransformer.ts index cd7b0e333..516c28b94 100644 --- a/src/components/TransactionDetails/transactionTransformer.ts +++ b/src/components/TransactionDetails/transactionTransformer.ts @@ -123,6 +123,7 @@ export interface TransactionDetails { points?: number isRequestPotLink?: boolean requestPotPayments?: ChargeEntry[] + totalAmountCollected: number } /** @@ -518,6 +519,7 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact haveSentMoneyToUser: entry.extraData?.haveSentMoneyToUser as boolean, isRequestPotLink: entry.isRequestLink, requestPotPayments: entry.charges, + totalAmountCollected: entry.totalAmountCollected ?? 0, } return { diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts index 0d3c104be..7210e6387 100644 --- a/src/utils/general.utils.ts +++ b/src/utils/general.utils.ts @@ -380,8 +380,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 }) } /** diff --git a/src/utils/history.utils.ts b/src/utils/history.utils.ts index 57097f7f7..843583d6c 100644 --- a/src/utils/history.utils.ts +++ b/src/utils/history.utils.ts @@ -131,6 +131,7 @@ export type HistoryEntry = { points?: number isRequestLink?: boolean // true if the transaction is a request pot link charges?: ChargeEntry[] + totalAmountCollected?: number } export function isFinalState(transaction: Pick): boolean { From 7bcf5a2632a6f56924a7d9e02d0b42a4566e874a Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Sat, 18 Oct 2025 19:11:16 +0530 Subject: [PATCH 16/28] feat: history changes --- src/app/(mobile-ui)/qr-pay/page.tsx | 2 + src/components/Global/Badges/StatusBadge.tsx | 5 +- src/components/Global/StatusPill/index.tsx | 3 + src/components/Home/HomeHistory.tsx | 2 +- .../TransactionDetailsHeaderCard.tsx | 4 +- .../TransactionDetailsReceipt.tsx | 60 ++++++++++++------- .../transaction-details.utils.ts | 2 + .../transactionTransformer.ts | 3 + src/utils/history.utils.ts | 2 + 9 files changed, 59 insertions(+), 24 deletions(-) diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index bbee3979f..93a1dc59a 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -938,6 +938,7 @@ export default function QRPayPage() { exchange_rate: currency.price.toString(), }, }, + totalAmountCollected: Number(usdAmount), }) }} > @@ -1019,6 +1020,7 @@ export default function QRPayPage() { exchange_rate: currency.price.toString(), }, }, + totalAmountCollected: Number(usdAmount), }) }} > diff --git a/src/components/Global/Badges/StatusBadge.tsx b/src/components/Global/Badges/StatusBadge.tsx index 963786ff4..55c3ff751 100644 --- a/src/components/Global/Badges/StatusBadge.tsx +++ b/src/components/Global/Badges/StatusBadge.tsx @@ -1,7 +1,7 @@ import React from 'react' import { twMerge } from 'tailwind-merge' -export type StatusType = 'completed' | 'pending' | 'failed' | 'cancelled' | 'soon' | 'processing' | 'custom' +export type StatusType = 'completed' | 'pending' | 'failed' | 'cancelled' | 'soon' | 'processing' | 'custom' | 'closed' interface StatusBadgeProps { status: StatusType @@ -14,6 +14,7 @@ const StatusBadge: React.FC = ({ status, className, size = 'sm const getStatusStyles = () => { switch (status) { case 'completed': + case 'closed': return 'bg-success-2 text-success-4 border border-success-5' case 'pending': case 'processing': @@ -43,6 +44,8 @@ const StatusBadge: React.FC = ({ status, className, size = 'sm return 'Cancelled' case 'soon': return 'Soon!' + case 'closed': + return 'Closed' case 'custom': return customText default: diff --git a/src/components/Global/StatusPill/index.tsx b/src/components/Global/StatusPill/index.tsx index d0271a365..ebadedf26 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,6 +26,7 @@ const StatusPill = ({ status }: StatusPillProps) => { soon: 'pending', pending: 'pending', cancelled: 'cancel', + closed: 'success', } const iconSize: Record = { @@ -34,6 +36,7 @@ const StatusPill = ({ status }: StatusPillProps) => { soon: 7, pending: 8, cancelled: 6, + closed: 7, } return ( diff --git a/src/components/Home/HomeHistory.tsx b/src/components/Home/HomeHistory.tsx index 03f99d320..e8ccd3e59 100644 --- a/src/components/Home/HomeHistory.tsx +++ b/src/components/Home/HomeHistory.tsx @@ -26,7 +26,7 @@ const HomeHistory = ({ isPublic = false, username }: { isPublic?: boolean; usern const isLoggedIn = !!user?.user.userId || false // fetch the latest 5 transaction history entries const mode = isPublic ? 'public' : 'latest' - const limit = isPublic ? 20 : 5 + const limit = isPublic ? 20 : 20 // Only filter when user is requesting for some different user's history const filterMutualTxs = !isPublic && username !== user?.user.username const { diff --git a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx index 8886b594d..126aa6c47 100644 --- a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx +++ b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx @@ -45,6 +45,7 @@ interface TransactionDetailsHeaderCardProps { progress?: number goal?: number isRequestPotTransaction?: boolean + isTransactionClosed: boolean } const getTitle = ( @@ -181,6 +182,7 @@ export const TransactionDetailsHeaderCard: React.FC { const router = useRouter() const typeForAvatar = @@ -254,7 +256,7 @@ export const TransactionDetailsHeaderCard: React.FC {!isNoGoalSet && showProgessBar && goal !== undefined && progress !== undefined && (
- +
)} diff --git a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx index cb89bb41f..0b096d6e2 100644 --- a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx +++ b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx @@ -50,6 +50,7 @@ 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, @@ -180,6 +181,7 @@ export const TransactionDetailsReceipt = ({ !isPublic && transaction.extraDataForDrawer?.originalType === EHistoryEntryType.MANTECA_ONRAMP && transaction.status === 'pending', + closed: !!(transaction.status === 'closed' && transaction.cancelledDate), } }, [transaction, isPendingBankRequest]) @@ -353,6 +355,28 @@ 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) + } + } + } + return (
{/* show qr code at the top if applicable */} @@ -395,6 +419,7 @@ export const TransactionDetailsReceipt = ({ goal={Number(transaction.amount)} progress={Number(formattedTotalAmountCollected)} isRequestPotTransaction={transaction.isRequestPotLink} + isTransactionClosed={transaction.status === 'closed'} /> {/* details card (date, fee, memo) and more */} @@ -519,6 +544,18 @@ export const TransactionDetailsReceipt = ({ )} + {rowVisibilityConfig.closed && ( + <> + {transaction.cancelledDate && ( + + )} + + )} + {rowVisibilityConfig.claimed && ( <> {transaction.claimedAt && ( @@ -979,31 +1016,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'}
)} 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 516c28b94..de32e90e3 100644 --- a/src/components/TransactionDetails/transactionTransformer.ts +++ b/src/components/TransactionDetails/transactionTransformer.ts @@ -398,6 +398,9 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact case 'EXPIRED': uiStatus = 'cancelled' break + case 'CLOSED': + uiStatus = 'closed' + break default: { const knownStatuses: StatusType[] = [ diff --git a/src/utils/history.utils.ts b/src/utils/history.utils.ts index 843583d6c..673a9b706 100644 --- a/src/utils/history.utils.ts +++ b/src/utils/history.utils.ts @@ -71,6 +71,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 +82,7 @@ export const FINAL_STATES: HistoryStatus[] = [ EHistoryStatus.REFUNDED, EHistoryStatus.CANCELED, EHistoryStatus.ERROR, + EHistoryStatus.CLOSED, ] export type HistoryEntryType = `${EHistoryEntryType}` From 669162cbed4fdceae1f6599d655e2f9e2432d0f4 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Sat, 18 Oct 2025 19:43:42 +0530 Subject: [PATCH 17/28] fix: txn receipts --- src/assets/icons/coin.svg | 8 ++++++++ src/assets/icons/index.ts | 1 + .../Global/Contributors/ContributorCard.tsx | 9 ++++++--- src/components/Global/ProgressBar/index.tsx | 3 +++ .../TransactionDetailsReceipt.tsx | 2 +- src/utils/general.utils.ts | 17 +++++++++++------ 6 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 src/assets/icons/coin.svg diff --git a/src/assets/icons/coin.svg b/src/assets/icons/coin.svg new file mode 100644 index 000000000..ce0b6c9e6 --- /dev/null +++ b/src/assets/icons/coin.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index 1db64201d..613f6a642 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -22,3 +22,4 @@ export { default as WHATSAPP_ICON } from './whatsapp.svg' export { default as IMESSAGE_ICON } from './imessage.svg' export { default as FBMessenger_ICON } from './fbmessenger.svg' export { default as SOCIALS_ICON } from './socials.svg' +export { default as COIN_ICON } from './coin.svg' diff --git a/src/components/Global/Contributors/ContributorCard.tsx b/src/components/Global/Contributors/ContributorCard.tsx index 89d0d0b70..2888c3169 100644 --- a/src/components/Global/Contributors/ContributorCard.tsx +++ b/src/components/Global/Contributors/ContributorCard.tsx @@ -4,6 +4,7 @@ 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' export type Contributor = { uuid: string @@ -16,6 +17,7 @@ export type Contributor = { const ContributorCard = ({ contributor, position }: { contributor: Contributor; position: CardPosition }) => { const colors = getColorForUsername(contributor.username ?? '') + const isEvmAddress = isAddress(contributor.username ?? '') return (
@@ -23,8 +25,9 @@ const ContributorCard = ({ contributor, position }: { contributor: Contributor;
-

{formatTokenAmount(Number(contributor.amount))}

+

${formatTokenAmount(Number(contributor.amount))}

) diff --git a/src/components/Global/ProgressBar/index.tsx b/src/components/Global/ProgressBar/index.tsx index 16561dd6f..ae987f769 100644 --- a/src/components/Global/ProgressBar/index.tsx +++ b/src/components/Global/ProgressBar/index.tsx @@ -1,3 +1,5 @@ +import { COIN_ICON } from '@/assets' +import Image from 'next/image' import React from 'react' import { twMerge } from 'tailwind-merge' @@ -33,6 +35,7 @@ const ProgressBar: React.FC = ({ goal, progress, isClosed }) = if (!isClosed) return null return (
+ coin

{getStatusText()}

) diff --git a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx index 0b096d6e2..8b7956737 100644 --- a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx +++ b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx @@ -251,7 +251,7 @@ export const TransactionDetailsReceipt = ({ return getContributorsFromCharge(transaction.requestPotPayments) }, [transaction]) - const formattedTotalAmountCollected = formatCurrency(transaction?.totalAmountCollected.toString() ?? '0', 2, 0) + const formattedTotalAmountCollected = formatCurrency(transaction?.totalAmountCollected?.toString() ?? '0', 2, 0) useEffect(() => { const getTokenDetails = async () => { diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts index 7210e6387..084c35222 100644 --- a/src/utils/general.utils.ts +++ b/src/utils/general.utils.ts @@ -1359,16 +1359,21 @@ export const getValidRedirectUrl = (redirectUrl: string, fallbackRoute: string) } export const getContributorsFromCharge = (charges: ChargeEntry[]) => { - // Contributors are the users who have paid for the charge - return charges - .filter((charge) => charge.fulfillmentPayment) - .map((charge) => ({ + return charges.map((charge) => { + const successfulPayment = charge.payments[charge.payments.length - 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: charge.payments[charge.payments.length - 1].payerAccount?.user?.username, + username, fulfilmentPayment: charge.fulfillmentPayment, isUserVerified: charge.payments[charge.payments.length - 1].payerAccount?.user?.bridgeKycStatus === 'approved', - })) + } + }) } From 04f390feb84092775d817a5bba18932a12c780ca Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Sat, 18 Oct 2025 20:11:21 +0530 Subject: [PATCH 18/28] Fix: minor UI changes --- src/components/Global/ProgressBar/index.tsx | 7 +++++-- src/components/TransactionDetails/TransactionCard.tsx | 4 +--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/Global/ProgressBar/index.tsx b/src/components/Global/ProgressBar/index.tsx index ae987f769..0e777bf8b 100644 --- a/src/components/Global/ProgressBar/index.tsx +++ b/src/components/Global/ProgressBar/index.tsx @@ -66,11 +66,14 @@ const ProgressBar: React.FC = ({ goal, progress, isClosed }) = return (
-

+

{formatCurrency(progress)}

-

{percentage}%

+

100%

) diff --git a/src/components/TransactionDetails/TransactionCard.tsx b/src/components/TransactionDetails/TransactionCard.tsx index 853249334..d2f02ba1e 100644 --- a/src/components/TransactionDetails/TransactionCard.tsx +++ b/src/components/TransactionDetails/TransactionCard.tsx @@ -167,9 +167,7 @@ const TransactionCard: React.FC = ({
)}
- - {displayAmount} - + {displayAmount} {currencyDisplayAmount && ( {currencyDisplayAmount} )} From 666e3a35c78172ced8358de1ea2b5d42574a96ca Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Sat, 18 Oct 2025 20:14:18 +0530 Subject: [PATCH 19/28] refactor: use explicit type import for ChargeEntry in history.utils.ts --- src/utils/history.utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/history.utils.ts b/src/utils/history.utils.ts index c5edc6878..cd2484a80 100644 --- a/src/utils/history.utils.ts +++ b/src/utils/history.utils.ts @@ -6,7 +6,7 @@ import { formatUnits } from 'viem' import { type Hash } from 'viem' import { getTokenDetails } from '@/utils' import { getCurrencyPrice } from '@/app/actions/currency' -import { ChargeEntry } from '@/services/services.types' +import { type ChargeEntry } from '@/services/services.types' export enum EHistoryEntryType { REQUEST = 'REQUEST', From 92facb2e974679feac7cc2d4516336f3a61080e6 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Sat, 18 Oct 2025 20:21:01 +0530 Subject: [PATCH 20/28] refactor: use explicit type imports for Payment and ChargeEntry in ContributorCard and index components --- src/components/Global/Contributors/ContributorCard.tsx | 4 ++-- src/components/Global/Contributors/index.tsx | 2 +- src/utils/general.utils.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Global/Contributors/ContributorCard.tsx b/src/components/Global/Contributors/ContributorCard.tsx index 2888c3169..981e0784c 100644 --- a/src/components/Global/Contributors/ContributorCard.tsx +++ b/src/components/Global/Contributors/ContributorCard.tsx @@ -1,5 +1,5 @@ -import { ChargeEntry, Payment } from '@/services/services.types' -import Card, { CardPosition } from '../Card' +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' diff --git a/src/components/Global/Contributors/index.tsx b/src/components/Global/Contributors/index.tsx index d2641bb78..a8844fa7c 100644 --- a/src/components/Global/Contributors/index.tsx +++ b/src/components/Global/Contributors/index.tsx @@ -1,4 +1,4 @@ -import { ChargeEntry } from '@/services/services.types' +import { type ChargeEntry } from '@/services/services.types' import { getContributorsFromCharge } from '@/utils' import React from 'react' import ContributorCard from './ContributorCard' diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts index f08b1c8b9..31356e1a4 100644 --- a/src/utils/general.utils.ts +++ b/src/utils/general.utils.ts @@ -10,7 +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 { ChargeEntry } from '@/services/services.types' +import { type ChargeEntry } from '@/services/services.types' export function urlBase64ToUint8Array(base64String: string) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4) From 4d6db97538e5755ddda6baa14d591d239f7c2f11 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Sun, 19 Oct 2025 12:31:03 +0530 Subject: [PATCH 21/28] fix: update transaction status logic to treat zero total amount as cancelled --- src/components/TransactionDetails/transactionTransformer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/TransactionDetails/transactionTransformer.ts b/src/components/TransactionDetails/transactionTransformer.ts index 1852e8df7..e630f8760 100644 --- a/src/components/TransactionDetails/transactionTransformer.ts +++ b/src/components/TransactionDetails/transactionTransformer.ts @@ -399,7 +399,8 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact uiStatus = 'cancelled' break case 'CLOSED': - uiStatus = 'closed' + // If the total amount collected is 0, the link is treated as cancelled + uiStatus = entry.totalAmountCollected === 0 ? 'cancelled' : 'closed' break default: { From 139bc4fb7dd19b9d5054e4b9108d2f2121eabc56 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Sun, 19 Oct 2025 20:43:02 +0530 Subject: [PATCH 22/28] request fulfillment page final changes --- src/app/[...recipient]/client.tsx | 2 +- .../Global/Contributors/ContributorCard.tsx | 3 +-- src/components/Payment/PaymentForm/index.tsx | 13 +------------ src/services/services.types.ts | 1 + src/utils/general.utils.ts | 2 +- 5 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/app/[...recipient]/client.tsx b/src/app/[...recipient]/client.tsx index 2d57b7e51..d1578a2c9 100644 --- a/src/app/[...recipient]/client.tsx +++ b/src/app/[...recipient]/client.tsx @@ -524,7 +524,7 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props) isRequestPotPayment={!!requestId} />
- {chargeId && showActionList && ( + {!requestId && showActionList && (
diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index 3e556924a..0ece3cd3f 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -384,7 +384,6 @@ export const PaymentForm = ({ return } - console.log('hello') if (!canInitiatePayment) return // regular payment flow @@ -440,13 +439,6 @@ export const PaymentForm = ({ isRequestPotPayment, } - // if (isRequestPotPayment) { - // const res = await createRequestCharge(payload) - // console.log(res) - // dispatch(paymentActions.setView('CONFIRM')) - // return - // } - console.log('Initiating payment with payload:', payload) const result = await initiatePayment(payload) @@ -629,10 +621,7 @@ export const PaymentForm = ({ const contributors = getContributorsFromCharge(requestDetails?.charges || []) - const totalAmountCollected = useMemo(() => { - if (!requestDetails?.charges || !requestDetails?.charges.length) return 0 - return contributors.reduce((acc, curr) => acc + Number(curr.amount), 0) - }, [contributors]) + const totalAmountCollected = requestDetails?.totalCollectedAmount ?? 0 if (fulfillUsingManteca && chargeDetails) { return diff --git a/src/services/services.types.ts b/src/services/services.types.ts index ac7e46782..afa8e2a9b 100644 --- a/src/services/services.types.ts +++ b/src/services/services.types.ts @@ -45,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 31356e1a4..3004e9e87 100644 --- a/src/utils/general.utils.ts +++ b/src/utils/general.utils.ts @@ -1360,7 +1360,7 @@ export const getContributorsFromCharge = (charges: ChargeEntry[]) => { payments: charge.payments, amount: charge.tokenAmount, username, - fulfilmentPayment: charge.fulfillmentPayment, + fulfillmentPayment: charge.fulfillmentPayment, isUserVerified: charge.payments[charge.payments.length - 1].payerAccount?.user?.bridgeKycStatus === 'approved', } From 6468c929a2b7070842935a2ce59fb26e8002227d Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Sun, 19 Oct 2025 21:03:47 +0530 Subject: [PATCH 23/28] fix: cr suggestions --- src/components/Global/ProgressBar/index.tsx | 11 ++++++----- src/components/Global/TokenAmountInput/index.tsx | 12 ++++++++---- src/components/Home/HomeHistory.tsx | 2 +- .../Request/link/views/Create.request.link.view.tsx | 2 +- src/components/User/UserCard.tsx | 2 +- src/hooks/usePaymentInitiator.ts | 6 ++++++ src/services/requests.ts | 12 ++++++++++-- src/utils/general.utils.ts | 9 ++++----- 8 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/components/Global/ProgressBar/index.tsx b/src/components/Global/ProgressBar/index.tsx index 0e777bf8b..2c13b127f 100644 --- a/src/components/Global/ProgressBar/index.tsx +++ b/src/components/Global/ProgressBar/index.tsx @@ -10,13 +10,14 @@ interface ProgressBarProps { } const ProgressBar: React.FC = ({ goal, progress, isClosed }) => { - const isOverGoal = progress > goal - const isGoalAchieved = progress >= goal && !isOverGoal + const isOverGoal = progress > goal && goal > 0 + const isGoalAchieved = progress >= goal && !isOverGoal && goal > 0 const totalValue = isOverGoal ? progress : goal - const goalPercentage = (goal / totalValue) * 100 - const progressPercentage = (progress / totalValue) * 100 - const percentage = Math.round((progress / goal) * 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 percentage = goal > 0 ? Math.min(Math.max(Math.round((progress / goal) * 100), 0), 100) : 0 const formatCurrency = (value: number) => `$${value.toFixed(2)}` diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx index fff7c4a56..83a12666d 100644 --- a/src/components/Global/TokenAmountInput/index.tsx +++ b/src/components/Global/TokenAmountInput/index.tsx @@ -213,6 +213,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() @@ -306,10 +313,7 @@ const TokenAmountInput = ({ )} {showSlider && maxAmount && (
- +
)} diff --git a/src/components/Home/HomeHistory.tsx b/src/components/Home/HomeHistory.tsx index 2a949449f..9c3f0bfc4 100644 --- a/src/components/Home/HomeHistory.tsx +++ b/src/components/Home/HomeHistory.tsx @@ -26,7 +26,7 @@ const HomeHistory = ({ isPublic = false, username }: { isPublic?: boolean; usern const isLoggedIn = !!user?.user.userId || false // fetch the latest 5 transaction history entries const mode = isPublic ? 'public' : 'latest' - const limit = isPublic ? 20 : 20 + const limit = isPublic ? 20 : 5 // Only filter when user is requesting for some different user's history const filterMutualTxs = !isPublic && username !== user?.user.username const { 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 483270fbf..a2e82614d 100644 --- a/src/components/Request/link/views/Create.request.link.view.tsx +++ b/src/components/Request/link/views/Create.request.link.view.tsx @@ -385,7 +385,7 @@ export const CreateRequestLinkView = () => { ) : ( - {tokenValue.length === 0 || parseFloat(tokenValue) === 0 + {!tokenValue || !parseFloat(tokenValue) || parseFloat(tokenValue) === 0 ? 'Share open request' : `Share $${tokenValue} request`} diff --git a/src/components/User/UserCard.tsx b/src/components/User/UserCard.tsx index e5a207d52..e0a96ecb8 100644 --- a/src/components/User/UserCard.tsx +++ b/src/components/User/UserCard.tsx @@ -100,7 +100,7 @@ const UserCard = ({ ) : ( { if (currentUrl.searchParams.get('chargeId') === activeChargeDetails.uuid) { const newUrl = new URL(window.location.href) newUrl.searchParams.delete('chargeId') + // Use router.replace (not window.history.replaceState) so that + // the components using the search params will be updated router.replace(newUrl.pathname + newUrl.search) } } @@ -394,6 +396,8 @@ export const usePaymentInitiator = () => { const newUrl = new URL(window.location.href) if (payload.requestId) newUrl.searchParams.delete('id') newUrl.searchParams.set('chargeId', chargeDetailsToUse.uuid) + // Use router.replace (not window.history.replaceState) so that + // the components using the search params will be updated router.replace(newUrl.pathname + newUrl.search) console.log('Updated URL with chargeId:', newUrl.href) } @@ -673,6 +677,8 @@ export const usePaymentInitiator = () => { if (currentUrl.searchParams.get('chargeId') === determinedChargeDetails.uuid) { const newUrl = new URL(window.location.href) newUrl.searchParams.delete('chargeId') + // Use router.replace (not window.history.replaceState) so that + // the components using the search params will be updated router.replace(newUrl.pathname + newUrl.search) console.log('URL updated, chargeId removed.') } diff --git a/src/services/requests.ts b/src/services/requests.ts index f3ef9f8fa..f7932af47 100644 --- a/src/services/requests.ts +++ b/src/services/requests.ts @@ -5,11 +5,15 @@ import Cookies from 'js-cookie' export const requestsApi = { create: async (data: CreateRequestRequest): 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`, { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${Cookies.get('jwt-token')}`, + Authorization: `Bearer ${token}`, }, body: jsonStringify(data), }) @@ -34,11 +38,15 @@ export const requestsApi = { }, update: async (id: string, data: Partial): 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/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${Cookies.get('jwt-token')}`, + Authorization: `Bearer ${token}`, }, body: jsonStringify(data), }) diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts index 3004e9e87..496f86d71 100644 --- a/src/utils/general.utils.ts +++ b/src/utils/general.utils.ts @@ -1349,9 +1349,9 @@ export const getValidRedirectUrl = (redirectUrl: string, fallbackRoute: string) export const getContributorsFromCharge = (charges: ChargeEntry[]) => { return charges.map((charge) => { - const successfulPayment = charge.payments[charge.payments.length - 1] - let username = successfulPayment.payerAccount?.user?.username - if (successfulPayment.payerAccount?.type === 'evm-address') { + const successfulPayment = charge.payments.at(-1) + let username = successfulPayment?.payerAccount?.user?.username + if (successfulPayment?.payerAccount?.type === 'evm-address') { username = successfulPayment.payerAccount.identifier } @@ -1361,8 +1361,7 @@ export const getContributorsFromCharge = (charges: ChargeEntry[]) => { amount: charge.tokenAmount, username, fulfillmentPayment: charge.fulfillmentPayment, - isUserVerified: - charge.payments[charge.payments.length - 1].payerAccount?.user?.bridgeKycStatus === 'approved', + isUserVerified: successfulPayment?.payerAccount?.user?.bridgeKycStatus === 'approved', } }) } From 07149b8cced07cfbf1201327fe10b3d2bddadb7b Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Sun, 19 Oct 2025 21:20:21 +0530 Subject: [PATCH 24/28] fix: update amount handling for request pot payments in PaymentForm --- src/components/Payment/PaymentForm/index.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index 0ece3cd3f..6bb57d61f 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -473,6 +473,7 @@ export const PaymentForm = ({ requestedTokenPrice, inviteError, handleAcceptInvite, + isRequestPotPayment, ]) const getButtonText = () => { @@ -663,8 +664,8 @@ export const PaymentForm = ({ fileUrl={requestDetails?.attachmentUrl || chargeDetails?.requestLink?.attachmentUrl || ''} isVerified={recipientKycStatus === 'approved'} haveSentMoneyToUser={recipientUserId ? interactions[recipientUserId] || false : false} - amount={isRequestPotPayment ? Number(amount) : undefined} - amountCollected={isRequestPotPayment ? totalAmountCollected : undefined} // TODO + amount={isRequestPotPayment && amount ? Number(amount) : undefined} + amountCollected={isRequestPotPayment ? totalAmountCollected : undefined} isRequestPot={isRequestPotPayment} /> )} @@ -688,8 +689,8 @@ export const PaymentForm = ({ currency={currency} hideCurrencyToggle={!currency} hideBalance={isExternalWalletFlow} - showSlider={isRequestPotPayment && Number(amount) > 0} - maxAmount={isRequestPotPayment ? Number(amount) : undefined} + showSlider={isRequestPotPayment && amount ? Number(amount) > 0 : false} + maxAmount={isRequestPotPayment && amount ? Number(amount) : undefined} /> {/* From f9dd8830b2eb10a851f98e006296f33d6ef8c7b8 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Sun, 19 Oct 2025 23:31:44 +0530 Subject: [PATCH 25/28] fix: nitpicks --- src/app/[...recipient]/client.tsx | 2 +- src/components/Payment/PaymentForm/index.tsx | 58 +++++++++++--------- src/hooks/usePaymentInitiator.ts | 16 +++--- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/app/[...recipient]/client.tsx b/src/app/[...recipient]/client.tsx index d1578a2c9..7f96fb09d 100644 --- a/src/app/[...recipient]/client.tsx +++ b/src/app/[...recipient]/client.tsx @@ -521,7 +521,7 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props) } setCurrencyAmount={(value: string | undefined) => setCurrencyAmount(value || '')} currencyAmount={currencyAmount} - isRequestPotPayment={!!requestId} + showRequestPotInitialView={!!requestId} />
{!requestId && showActionList && ( diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index 6bb57d61f..7c5c8bde1 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -51,7 +51,7 @@ export type PaymentFlowProps = { setCurrencyAmount?: (currencyvalue: string | undefined) => void headerTitle?: string flow?: PaymentFlow - isRequestPotPayment?: boolean + showRequestPotInitialView?: boolean } export type PaymentFormProps = ParsedURL & PaymentFlowProps @@ -68,7 +68,7 @@ export const PaymentForm = ({ isDirectUsdPayment, headerTitle, flow, - isRequestPotPayment, + showRequestPotInitialView, }: PaymentFormProps) => { const dispatch = useAppDispatch() const router = useRouter() @@ -214,7 +214,7 @@ export const PaymentForm = ({ } else { // regular send/pay if ( - !isRequestPotPayment && + !showRequestPotInitialView && // don't apply balance check on request pot payment initial view isActivePeanutWallet && areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN) ) { @@ -269,7 +269,7 @@ export const PaymentForm = ({ selectedTokenData, isExternalWalletConnected, isExternalWalletFlow, - isRequestPotPayment, + showRequestPotInitialView, ]) // fetch token price @@ -318,8 +318,8 @@ export const PaymentForm = ({ const walletConnected = isConnected // If its requestPotPayment, we only need to check if the recipient exists, amount is set, and token is selected - if (isRequestPotPayment) { - return recipientExists && amountIsSet && tokenSelected && isRequestPotPayment + if (showRequestPotInitialView) { + return recipientExists && amountIsSet && tokenSelected && showRequestPotInitialView } return recipientExists && amountIsSet && tokenSelected && walletConnected @@ -364,7 +364,8 @@ export const PaymentForm = ({ if (inviteError) { setInviteError(false) } - if (!isRequestPotPayment && 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() @@ -374,12 +375,14 @@ export const PaymentForm = ({ return } - if (!isRequestPotPayment && !isExternalWalletConnected && isExternalWalletFlow) { + // skip this step for request pots initial view + if (!showRequestPotInitialView && !isExternalWalletConnected && isExternalWalletFlow) { openReownModal() return } - if (!isRequestPotPayment && !isConnected) { + // skip this step for request pots initial view + if (!showRequestPotInitialView && !isConnected) { dispatch(walletActions.setSignInModalVisible(true)) return } @@ -436,7 +439,7 @@ export const PaymentForm = ({ ? 'DIRECT_SEND' : 'REQUEST', attachmentOptions: attachmentOptions, - isRequestPotPayment, + returnAfterChargeCreation: !!showRequestPotInitialView, // For request pot initial view, return after charge creation without initiating payment } console.log('Initiating payment with payload:', payload) @@ -446,7 +449,7 @@ export const PaymentForm = ({ if (result.status === 'Success') { dispatch(paymentActions.setView('STATUS')) } else if (result.status === 'Charge Created') { - if (!fulfillUsingManteca && !isRequestPotPayment) { + if (!fulfillUsingManteca && !showRequestPotInitialView) { dispatch(paymentActions.setView('CONFIRM')) } } else if (result.status === 'Error') { @@ -473,7 +476,7 @@ export const PaymentForm = ({ requestedTokenPrice, inviteError, handleAcceptInvite, - isRequestPotPayment, + showRequestPotInitialView, ]) const getButtonText = () => { @@ -489,7 +492,7 @@ export const PaymentForm = ({ return 'Send' } - if (isRequestPotPayment) { + if (showRequestPotInitialView) { return 'Pay' } @@ -521,12 +524,12 @@ export const PaymentForm = ({ } const getButtonIcon = (): IconName | undefined => { - if (!isRequestPotPayment && !isExternalWalletConnected && isExternalWalletFlow) return 'wallet-outline' + if (!showRequestPotInitialView && !isExternalWalletConnected && isExternalWalletFlow) return 'wallet-outline' - if (!isRequestPotPayment && isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) + if (!showRequestPotInitialView && isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) return 'arrow-down' - if (!isRequestPotPayment && !isProcessing && isActivePeanutWallet && !isExternalWalletFlow) + if (!showRequestPotInitialView && !isProcessing && isActivePeanutWallet && !isExternalWalletFlow) return 'arrow-up-right' return undefined @@ -630,7 +633,9 @@ export const PaymentForm = ({ return (
- + {!showRequestPotInitialView && ( + + )}
{isExternalWalletConnected && isUsingExternalWallet && (
- {isRequestPotPayment && ( + {showRequestPotInitialView && (

Contributors ({contributors.length})

{contributors.map((contributor, index) => ( diff --git a/src/hooks/usePaymentInitiator.ts b/src/hooks/usePaymentInitiator.ts index b0e6f14b6..b6e2497e5 100644 --- a/src/hooks/usePaymentInitiator.ts +++ b/src/hooks/usePaymentInitiator.ts @@ -58,7 +58,7 @@ export interface InitiatePaymentPayload { isExternalWalletFlow?: boolean transactionType?: TChargeTransactionType attachmentOptions?: IAttachmentOptions - isRequestPotPayment?: boolean + returnAfterChargeCreation?: boolean } interface InitiationResult { @@ -161,9 +161,9 @@ export const usePaymentInitiator = () => { if (currentUrl.searchParams.get('chargeId') === activeChargeDetails.uuid) { const newUrl = new URL(window.location.href) newUrl.searchParams.delete('chargeId') - // Use router.replace (not window.history.replaceState) so that + // Use router.push (not window.history.replaceState) so that // the components using the search params will be updated - router.replace(newUrl.pathname + newUrl.search) + router.push(newUrl.pathname + newUrl.search) } } return { @@ -396,9 +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) - // Use router.replace (not window.history.replaceState) so that + // Use router.push (not window.history.replaceState) so that // the components using the search params will be updated - router.replace(newUrl.pathname + newUrl.search) + router.push(newUrl.pathname + newUrl.search) console.log('Updated URL with chargeId:', newUrl.href) } } @@ -622,7 +622,7 @@ export const usePaymentInitiator = () => { // 2. handle charge state if ( - payload.isRequestPotPayment || // For request pot payment, return after charge creation + payload.returnAfterChargeCreation || // For request pot payment, return after charge creation (chargeCreated && (payload.isExternalWalletFlow || !isPeanutWallet || @@ -677,9 +677,9 @@ export const usePaymentInitiator = () => { if (currentUrl.searchParams.get('chargeId') === determinedChargeDetails.uuid) { const newUrl = new URL(window.location.href) newUrl.searchParams.delete('chargeId') - // Use router.replace (not window.history.replaceState) so that + // Use router.push (not window.history.replaceState) so that // the components using the search params will be updated - router.replace(newUrl.pathname + newUrl.search) + router.push(newUrl.pathname + newUrl.search) console.log('URL updated, chargeId removed.') } } From fcfefbbf90eb715bd78dba939f74af1dff4e41c5 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Fri, 24 Oct 2025 12:38:11 +0530 Subject: [PATCH 26/28] fix: name --- .../TransactionDetails/TransactionDetailsHeaderCard.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx index d5fe5ee20..bfd88be9f 100644 --- a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx +++ b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx @@ -226,7 +226,11 @@ export const TransactionDetailsHeaderCard: React.FC} Date: Fri, 24 Oct 2025 14:59:52 +0530 Subject: [PATCH 27/28] feat: add bill split params --- .../link/views/Create.request.link.view.tsx | 32 ++++++------------- 1 file changed, 9 insertions(+), 23 deletions(-) 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 a2e82614d..7ead5dc57 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,15 @@ export const CreateRequestLinkView = () => { } = useContext(context.tokenSelectorContext) const { setLoadingState } = useContext(context.loadingStateContext) const queryClient = useQueryClient() + const searchParams = useSearchParams() + const paramsAmount = searchParams.get('amount') + const merchant = searchParams.get('merchant') + const merchantComment = merchant ? `Bill split for ${merchant}` : null // Core state - const [tokenValue, setTokenValue] = useState('') + const [tokenValue, setTokenValue] = useState(paramsAmount || '') const [attachmentOptions, setAttachmentOptions] = useState({ - message: '', + message: merchantComment || '', fileUrl: '', rawFile: undefined, }) @@ -255,17 +259,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 = @@ -276,15 +270,7 @@ export const CreateRequestLinkView = () => { await updateRequestLink(debouncedAttachmentOptions) } } - }, [ - debouncedAttachmentOptions, - requestId, - tokenValue, - isCreatingLink, - isUpdatingRequest, - createRequestLink, - updateRequestLink, - ]) + }, [debouncedAttachmentOptions, requestId, isCreatingLink, isUpdatingRequest, updateRequestLink]) useEffect(() => { handleDebouncedChange() From 7bc99ec53c46c3541801210f16539f1037563b90 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Fri, 24 Oct 2025 19:07:08 +0530 Subject: [PATCH 28/28] feat: add bill split button for QR payments --- src/components/Global/Icons/Icon.tsx | 3 +++ src/components/Global/Icons/split.tsx | 12 ++++++++++ .../link/views/Create.request.link.view.tsx | 3 ++- .../TransactionDetailsReceipt.tsx | 22 ++++++++++++++++++- 4 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/components/Global/Icons/split.tsx diff --git a/src/components/Global/Icons/Icon.tsx b/src/components/Global/Icons/Icon.tsx index 126e235fd..973a3b7c8 100644 --- a/src/components/Global/Icons/Icon.tsx +++ b/src/components/Global/Icons/Icon.tsx @@ -63,6 +63,7 @@ import { ShieldIcon } from './shield' import { TrophyIcon } from './trophy' import { InviteHeartIcon } from './invite-heart' import { LockIcon } from './lock' +import { SplitIcon } from './split' // available icon names export type IconName = @@ -130,6 +131,7 @@ export type IconName = | 'trophy' | 'invite-heart' | 'lock' + | 'split' export interface IconProps extends SVGProps { name: IconName size?: number | string @@ -201,6 +203,7 @@ const iconComponents: Record>> = trophy: TrophyIcon, 'invite-heart': InviteHeartIcon, lock: LockIcon, + split: SplitIcon, } export const Icon: FC = ({ name, size = 24, width, height, ...props }) => { diff --git a/src/components/Global/Icons/split.tsx b/src/components/Global/Icons/split.tsx new file mode 100644 index 000000000..218046f25 --- /dev/null +++ b/src/components/Global/Icons/split.tsx @@ -0,0 +1,12 @@ +import { type FC, type SVGProps } from 'react' + +export const SplitIcon: FC> = (props) => { + return ( + + + + ) +} 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 7ead5dc57..c2c94083a 100644 --- a/src/components/Request/link/views/Create.request.link.view.tsx +++ b/src/components/Request/link/views/Create.request.link.view.tsx @@ -43,11 +43,12 @@ export const CreateRequestLinkView = () => { 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(paramsAmount || '') + const [tokenValue, setTokenValue] = useState(sanitizedAmount) const [attachmentOptions, setAttachmentOptions] = useState({ message: merchantComment || '', fileUrl: '', diff --git a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx index 220384e4c..e7053c956 100644 --- a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx +++ b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx @@ -262,6 +262,12 @@ 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) @@ -1186,9 +1192,23 @@ export const TransactionDetailsReceipt = ({
)} + {isQRPayment && ( + + )} + {shouldShowShareReceipt && !!getReceiptUrl(transaction) && (
- Share Receipt + + Share Receipt +
)}