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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 15 additions & 17 deletions src/app/(mobile-ui)/qr-pay/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useSearchParams, useRouter } from 'next/navigation'
import { useState, useCallback, useMemo, useEffect, useContext } from 'react'
import { Card } from '@/components/0_Bruddle/Card'

import { Button } from '@/components/0_Bruddle/Button'
import { Icon } from '@/components/Global/Icons/Icon'
import { mantecaApi } from '@/services/manteca'
Expand All @@ -29,6 +29,8 @@ import { QrKycState, useQrKycGate } from '@/hooks/useQrKycGate'
import ActionModal from '@/components/Global/ActionModal'
import { saveRedirectUrl } from '@/utils/general.utils'
import { MantecaGeoSpecificKycModal } from '@/components/Kyc/InitiateMantecaKYCModal'
import { PeanutDoesntStoreAnyPersonalInformation } from '@/components/Kyc/KycVerificationInProgressModal'
import Card from '@/components/Global/Card'

const MANTECA_DEPOSIT_ADDRESS = '0x959e088a09f61aB01cb83b0eBCc74b2CF6d62053'
const MAX_QR_PAYMENT_AMOUNT = '200'
Expand Down Expand Up @@ -216,18 +218,18 @@ export default function QRPayPage() {
if (!!errorInitiatingPayment) {
return (
<div className="my-auto flex h-full flex-col justify-center space-y-4">
<Card className="shadow-4">
<Card.Header>
<Card.Title>Unable to get QR details</Card.Title>
<Card.Description>
<Card className="shadow-4 space-y-2">
<div className="space-y-2">
<h1 className="text-3xl font-extrabold">Unable to get QR details</h1>
<p className="text-lg">
{errorInitiatingPayment || 'An error occurred while getting the QR details.'}
</Card.Description>
</Card.Header>
<Card.Content>
<Button onClick={() => router.back()} variant="purple">
Go Back
</Button>
</Card.Content>
</p>
</div>
<div className="h-[1px] bg-black"></div>

<Button onClick={() => router.back()} variant="purple">
Go Back
</Button>
</Card>
</div>
)
Expand Down Expand Up @@ -273,11 +275,7 @@ export default function QRPayPage() {
icon: 'check-circle',
},
]}
footer={
<p className="flex items-center justify-center gap-1 text-xs text-gray-400">
<Icon name="info" className="size-3" /> Peanut doesn't store any personal information
</p>
}
footer={<PeanutDoesntStoreAnyPersonalInformation />}
/>
</div>
)
Expand Down
3 changes: 1 addition & 2 deletions src/app/[...recipient]/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,7 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props)
}, [transactionForDrawer, currentView, dispatch, openTransactionDetails, isExternalWalletFlow, chargeId])

const showActionList =
flow !== 'direct_pay' || // Always show for non-direct-pay flows
(flow === 'direct_pay' && !user) || // Show for direct-pay when user is not logged in
(flow !== 'direct_pay' || (flow === 'direct_pay' && !user)) && // Show for direct-pay when user is not logged in
!fulfillUsingManteca // Show when not fulfilling using Manteca
// Send to bank step if its mentioned in the URL and guest KYC is not needed
useEffect(() => {
Expand Down
52 changes: 49 additions & 3 deletions src/components/Claim/Link/MantecaFlowManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import NavHeader from '@/components/Global/NavHeader'
import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard'
import { useClaimBankFlow } from '@/context/ClaimBankFlowContext'
import { ClaimLinkData } from '@/services/sendLinks'
import { FC, useState } from 'react'
import { FC, useEffect, useState } from 'react'
import MantecaDetailsStep from './views/MantecaDetailsStep.view'
import { MercadoPagoStep } from '@/types/manteca.types'
import MantecaReviewStep from './views/MantecaReviewStep'
import { Button } from '@/components/0_Bruddle'
import { useRouter } from 'next/navigation'
import useKycStatus from '@/hooks/useKycStatus'
import { InitiateMantecaKYCModal } from '@/components/Kyc/InitiateMantecaKYCModal'
import { useAuth } from '@/context/authContext'
import { CountryData } from '@/components/AddMoney/consts'

interface MantecaFlowManagerProps {
claimLinkData: ClaimLinkData
Expand All @@ -19,12 +23,38 @@ interface MantecaFlowManagerProps {
}

const MantecaFlowManager: FC<MantecaFlowManagerProps> = ({ claimLinkData, amount, attachment }) => {
const { setClaimToMercadoPago } = useClaimBankFlow()
const { setClaimToMercadoPago, selectedCountry } = useClaimBankFlow()
const [currentStep, setCurrentStep] = useState<MercadoPagoStep>(MercadoPagoStep.DETAILS)
const router = useRouter()
const [destinationAddress, setDestinationAddress] = useState('')
const [isKYCModalOpen, setIsKYCModalOpen] = useState(false)
const argentinaCountryData = {
id: 'AR',
type: 'country',
title: 'Argentina',
currency: 'ARS',
path: 'argentina',
iso2: 'AR',
iso3: 'ARG',
} as CountryData

const { isUserMantecaKycApproved } = useKycStatus()
const { fetchUser } = useAuth()

const isSuccess = currentStep === MercadoPagoStep.SUCCESS
const selectedCurrency = selectedCountry?.currency || 'ARS'
const logo = selectedCountry?.id ? undefined : MERCADO_PAGO

const handleKycCancel = () => {
setIsKYCModalOpen(false)
onPrev()
}

useEffect(() => {
if (!isUserMantecaKycApproved) {
setIsKYCModalOpen(true)
}
}, [isUserMantecaKycApproved])

const renderStepDetails = () => {
if (currentStep === MercadoPagoStep.DETAILS) {
Expand All @@ -43,6 +73,7 @@ const MantecaFlowManager: FC<MantecaFlowManagerProps> = ({ claimLinkData, amount
claimLink={claimLinkData.link}
destinationAddress={destinationAddress}
amount={amount}
currency={selectedCurrency}
/>
)
}
Expand Down Expand Up @@ -88,10 +119,25 @@ const MantecaFlowManager: FC<MantecaFlowManagerProps> = ({ claimLinkData, amount
tokenSymbol={claimLinkData.tokenSymbol}
message={attachment.message}
fileUrl={attachment.attachmentUrl}
logo={isSuccess ? undefined : MERCADO_PAGO}
logo={isSuccess ? undefined : logo}
countryCodeForFlag={selectedCountry?.id.toLowerCase()}
/>
Comment on lines +123 to 124
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Runtime crash risk: optional chaining stops before toLowerCase().

Fix chaining to avoid calling toLowerCase on undefined.

-                    countryCodeForFlag={selectedCountry?.id.toLowerCase()}
+                    countryCodeForFlag={selectedCountry?.id?.toLowerCase()}

🏁 Script executed:

#!/bin/bash
rg -nP --type=ts --type=tsx -C1 '\?\.\s*id\.toLowerCase\(\)'

Length of output: 27


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search repository for patterns where optional chaining may stop before toLowerCase()
rg -n --hidden -S -C1 '\?\.\s*id\.toLowerCase\(\)' || true
rg -n --hidden -S -C1 'selectedCountry\?\.\s*id\.toLowerCase\(' || true
rg -n --hidden -S -C1 '\b\w+\?\.\s*id\s*\.toLowerCase\(' || true

# Try to locate the specific file from the review and print its contents (first 300 lines)
file=$(rg --files -S | rg 'MantecaFlowManager\.tsx' || true)
if [ -n "$file" ]; then
  echo "---- $file (first 300 lines) ----"
  sed -n '1,300p' "$file" | nl -ba -v1 -w6 -s': '
else
  echo "MantecaFlowManager.tsx not found; listing src/components/Claim/Link if it exists:"
  ls -la src/components/Claim/Link || true
fi

Length of output: 2995


Fix optional chaining before toLowerCase to prevent runtime crash.

selectedCountry?.id.toLowerCase() can call toLowerCase on undefined — use selectedCountry?.id?.toLowerCase().

  • src/components/Claim/Link/MantecaFlowManager.tsx (line 123): countryCodeForFlag={selectedCountry?.id?.toLowerCase()}
  • src/components/Payment/Views/MantecaFulfillment.view.tsx (lines 48 and 96): replace selectedCountry?.id.toLowerCase() with selectedCountry?.id?.toLowerCase()
🤖 Prompt for AI Agents
In src/components/Claim/Link/MantecaFlowManager.tsx around lines 123-124
(countryCodeForFlag={selectedCountry?.id.toLowerCase()}), and in
src/components/Payment/Views/MantecaFulfillment.view.tsx around lines 48 and 96,
optional chaining is applied to selectedCountry but not to its id before calling
toLowerCase, which can throw if id is undefined; change each occurrence to use
selectedCountry?.id?.toLowerCase() so the toLowerCase call is only invoked when
id exists, preserving undefined when either selectedCountry or id is missing.


{renderStepDetails()}

{isKYCModalOpen && (
<InitiateMantecaKYCModal
isOpen={isKYCModalOpen}
onClose={handleKycCancel}
onManualClose={handleKycCancel}
onKycSuccess={() => {
// close the modal and let the user continue with amount input
setIsKYCModalOpen(false)
fetchUser()
}}
country={selectedCountry || argentinaCountryData}
/>
)}
</div>
</div>
)
Expand Down
15 changes: 11 additions & 4 deletions src/components/Claim/Link/views/MantecaReviewStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,19 @@ interface MantecaReviewStepProps {
claimLink: string
destinationAddress: string
amount: string
currency: string
}

const MantecaReviewStep: FC<MantecaReviewStepProps> = ({ setCurrentStep, claimLink, destinationAddress, amount }) => {
const MantecaReviewStep: FC<MantecaReviewStepProps> = ({
setCurrentStep,
claimLink,
destinationAddress,
amount,
currency,
}) => {
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const { price, isLoading } = useCurrency('ARS') // TODO: change to the currency of the selected Method
const { price, isLoading } = useCurrency(currency)

const detailsCardRows: MantecaCardRow[] = [
{
Expand All @@ -31,7 +38,7 @@ const MantecaReviewStep: FC<MantecaReviewStepProps> = ({ setCurrentStep, claimLi
{
key: 'exchangeRate',
label: 'Exchange Rate',
value: `1 USD = ${price?.buy} ARS`,
value: `1 USD = ${price?.buy} ${currency}`,
},
{
key: 'fee',
Expand All @@ -55,7 +62,7 @@ const MantecaReviewStep: FC<MantecaReviewStepProps> = ({ setCurrentStep, claimLi
amount: amount.replace(/,/g, ''),
destinationAddress,
txHash,
currency: 'ARS', // TODO: source-selected currency
currency,
})
if (withdrawError || !data) {
setError(withdrawError || 'Something went wrong. Please contact Support')
Expand Down
2 changes: 1 addition & 1 deletion src/components/Common/CountryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export const CountryList = ({
isSupported = isBridgeSupportedCountry || isMantecaSupportedCountry
} else if (viewMode === 'claim-request') {
// support all countries
isSupported = isBridgeSupportedCountry
isSupported = isBridgeSupportedCountry || isMantecaSupportedCountry
} else {
// support all countries
isSupported = true
Expand Down
22 changes: 19 additions & 3 deletions src/components/Common/CountryListRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { formatUnits } from 'viem'
import { formatTokenAmount, printableAddress } from '@/utils/general.utils'
import { CountryList } from '@/components/Common/CountryList'
import { ClaimLinkData } from '@/services/sendLinks'
import { CountryData } from '@/components/AddMoney/consts'
import { CountryData, MantecaSupportedExchanges } from '@/components/AddMoney/consts'
import useSavedAccounts from '@/hooks/useSavedAccounts'
import { ParsedURL } from '@/lib/url-parser/types/payment'
import { useCallback, useMemo } from 'react'
Expand Down Expand Up @@ -41,23 +41,39 @@ export const CountryListRouter = ({
requestLinkData,
inputTitle,
}: ICountryListRouterViewProps) => {
const { setFlowStep: setClaimBankFlowStep, setSelectedCountry } = useClaimBankFlow()
const {
setFlowStep: setClaimBankFlowStep,
setSelectedCountry,
setClaimToMercadoPago,
setFlowStep,
} = useClaimBankFlow()
const {
setFlowStep: setRequestFulfilmentBankFlowStep,
setShowRequestFulfilmentBankFlowManager,
setSelectedCountry: setSelectedCountryForRequest,
setShowVerificationModal,
setFulfillUsingManteca,
} = useRequestFulfillmentFlow()
const savedAccounts = useSavedAccounts()
const { chargeDetails } = usePaymentStore()
const { requestType } = useDetermineBankRequestType(chargeDetails?.requestLink.recipientAccount.userId ?? '')
const { user } = useAuth()

const handleCountryClick = (country: CountryData) => {
const isMantecaSupportedCountry = Object.keys(MantecaSupportedExchanges).includes(country.id)
if (flow === 'claim') {
setSelectedCountry(country)
setClaimBankFlowStep(ClaimBankFlowStep.BankDetailsForm)
if (isMantecaSupportedCountry) {
setFlowStep(null) // reset the flow step to initial view first
setClaimToMercadoPago(true)
} else {
setClaimBankFlowStep(ClaimBankFlowStep.BankDetailsForm)
}
} else if (flow === 'request') {
if (isMantecaSupportedCountry) {
setShowRequestFulfilmentBankFlowManager(false)
setFulfillUsingManteca(true)
}
setSelectedCountryForRequest(country)

if (requestType === BankRequestType.PayerKycNeeded) {
Expand Down
17 changes: 11 additions & 6 deletions src/components/Global/PeanutActionDetailsCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,15 @@ export default function PeanutActionDetailsCard({
const isWithdrawBankAccount = transactionType === 'WITHDRAW_BANK_ACCOUNT' && recipientType === 'BANK_ACCOUNT'
const isAddBankAccount = transactionType === 'ADD_MONEY_BANK_ACCOUNT'
const isClaimLinkBankAccount = transactionType === 'CLAIM_LINK_BANK_ACCOUNT' && recipientType === 'BANK_ACCOUNT'
const isRegionalMethodClaim = transactionType === 'REGIONAL_METHOD_CLAIM'

const withdrawBankIcon = () => {
const imgSrc = logo ? logo : `https://flagcdn.com/w320/${countryCodeForFlag}.png`
if (isWithdrawBankAccount || isAddBankAccount || isClaimLinkBankAccount)

if (isWithdrawBankAccount || isAddBankAccount || isClaimLinkBankAccount || isRegionalMethodClaim)
return (
<div className="relative mr-1 h-12 w-12">
{countryCodeForFlag && (
{(countryCodeForFlag || logo) && (
<Image
src={imgSrc}
alt={`${countryCodeForFlag} flag`}
Expand All @@ -180,9 +182,11 @@ export default function PeanutActionDetailsCard({
className="h-12 w-12 rounded-full object-cover"
/>
)}
<div className="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-yellow-400 p-1.5">
<Icon name="bank" className="h-full w-full text-black" />
</div>
{!isRegionalMethodClaim && (
<div className="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-yellow-400 p-1.5">
<Icon name="bank" className="h-full w-full text-black" />
</div>
)}
</div>
)
return undefined
Expand All @@ -192,7 +196,8 @@ export default function PeanutActionDetailsCard({
<Card className={twMerge('p-4', className)}>
<div className="flex items-center gap-3">
<div className="flex items-center gap-3">
{viewType !== 'SUCCESS' && (isWithdrawBankAccount || isAddBankAccount || isClaimLinkBankAccount) ? (
{viewType !== 'SUCCESS' &&
(isWithdrawBankAccount || isAddBankAccount || isClaimLinkBankAccount || isRegionalMethodClaim) ? (
withdrawBankIcon()
) : (
<AvatarWithBadge
Expand Down
2 changes: 1 addition & 1 deletion src/components/Kyc/CountryRegionRow.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow'
import { CountryFlagAndName } from './Country-Flag-And-Name'
import { CountryFlagAndName } from './CountryFlagAndName'

interface CountryRegionRowProps {
countryCode?: string | null
Expand Down
Loading
Loading