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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 90 additions & 26 deletions src/app/(mobile-ui)/qr-pay/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import { useAuth } from '@/context/authContext'
import { useWebSocket } from '@/hooks/useWebSocket'
import type { HistoryEntry } from '@/hooks/useTransactionHistory'
import { completeHistoryEntry } from '@/utils/history.utils'
import { useSupportModalContext } from '@/context/SupportModalContext'
import chillPeanutAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_01.gif'

const MAX_QR_PAYMENT_AMOUNT = '2000'

Expand Down Expand Up @@ -84,6 +86,10 @@ export default function QRPayPage() {
const { user } = useAuth()
const [pendingSimpleFiPaymentId, setPendingSimpleFiPaymentId] = useState<string | null>(null)
const [isWaitingForWebSocket, setIsWaitingForWebSocket] = useState(false)
const [shouldRetry, setShouldRetry] = useState(true)
const { setIsSupportModalOpen } = useSupportModalContext()
const [waitingForMerchantAmount, setWaitingForMerchantAmount] = useState(false)
const retryCount = useRef(0)

const paymentProcessor: PaymentProcessor | null = useMemo(() => {
switch (qrType) {
Expand Down Expand Up @@ -132,12 +138,6 @@ export default function QRPayPage() {
}
}, [])

// Reset SimpleFi payment state
const resetSimpleFiState = () => {
setPendingSimpleFiPaymentId(null)
setIsWaitingForWebSocket(false)
}

const handleSimpleFiStatusUpdate = useCallback(
async (entry: HistoryEntry) => {
if (!pendingSimpleFiPaymentId || entry.uuid !== pendingSimpleFiPaymentId) {
Expand Down Expand Up @@ -387,15 +387,26 @@ export default function QRPayPage() {
useEffect(() => {
if (paymentProcessor !== 'MANTECA') return
if (!qrCode || !isPaymentProcessorQR(qrCode)) return
if (!!paymentLock) return
if (!!paymentLock || !shouldRetry) return

setShouldRetry(false)
setLoadingState('Fetching details')
mantecaApi
.initiateQrPayment({ qrCode })
.then((pl) => setPaymentLock(pl))
.catch((error) => setErrorInitiatingPayment(error.message))
.then((pl) => {
setWaitingForMerchantAmount(false)
setPaymentLock(pl)
})
.catch((error) => {
if (error.message.includes("provider can't decode it")) {
setWaitingForMerchantAmount(true)
Copy link
Contributor

Choose a reason for hiding this comment

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

nice

} else {
setErrorInitiatingPayment(error.message)
setWaitingForMerchantAmount(false)
}
})
.finally(() => setLoadingState('Idle'))
}, [paymentLock, qrCode, setLoadingState, paymentProcessor])
}, [paymentLock, qrCode, setLoadingState, paymentProcessor, shouldRetry])

const merchantName = useMemo(() => {
if (paymentProcessor === 'SIMPLEFI') {
Expand Down Expand Up @@ -723,7 +734,7 @@ export default function QRPayPage() {
}
}, [isSuccess])

const handleOrderNotReadyRetry = useCallback(async () => {
const handleSimplefiRetry = useCallback(async () => {
setShowOrderNotReadyModal(false)
if (!simpleFiQrData || simpleFiQrData.type !== 'SIMPLEFI_STATIC') return

Expand Down Expand Up @@ -753,6 +764,27 @@ export default function QRPayPage() {
}
}, [simpleFiQrData, setLoadingState])

useEffect(() => {
if (paymentProcessor !== 'SIMPLEFI') return
if (!shouldRetry) return
setShouldRetry(false)
handleSimplefiRetry()
}, [shouldRetry, handleSimplefiRetry])

useEffect(() => {
if (waitingForMerchantAmount && !shouldRetry) {
if (retryCount.current < 3) {
retryCount.current++
setTimeout(() => {
setShouldRetry(true)
}, 3000)
} else {
setWaitingForMerchantAmount(false)
setShowOrderNotReadyModal(true)
}
}
}, [waitingForMerchantAmount, shouldRetry])

if (!!errorInitiatingPayment) {
return (
<div className="my-auto flex h-full flex-col justify-center space-y-4">
Expand Down Expand Up @@ -840,25 +872,57 @@ export default function QRPayPage() {
)
}

if (showOrderNotReadyModal) {
if (waitingForMerchantAmount) {
return (
<div className="my-auto flex h-full flex-col justify-center space-y-4">
<Card className="shadow-4 space-y-2">
<div className="space-y-2">
<h1 className="text-3xl font-extrabold">Order Not Ready</h1>
<p className="text-lg">Please notify the cashier that you're ready to pay, then tap Retry.</p>
</div>
<div className="h-[1px] bg-black"></div>
<div className="my-auto flex h-full w-full flex-col items-center justify-center space-y-4">
<div className="relative">
<Image
src={chillPeanutAnim.src}
alt="Peanut Mascot"
width={20}
height={20}
className="absolute z-0 h-32 w-32 -translate-y-20 translate-x-26"
/>
<Card className="relative z-10 flex w-full flex-col items-center gap-4 p-4">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-secondary-1 p-3">
<Icon name="clock" className="h-full" />
</div>
<p className="font-medium">Waiting for the merchant to set the amount</p>
</Card>
</div>
</div>
)
}

<div className="flex flex-col space-y-3">
<Button onClick={handleOrderNotReadyRetry} variant="purple" shadowSize="4">
Retry Payment
</Button>
<Button onClick={() => router.back()} variant="primary-soft" shadowSize="4">
Cancel
</Button>
if (showOrderNotReadyModal) {
return (
<div className="my-auto flex h-full w-full flex-col justify-center space-y-4">
<Card className="flex w-full flex-col items-center gap-2 p-4">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-secondary-1 p-3">
<Icon name="qr-code" className="h-full" />
</div>
<span className="text-lg font-bold">We couldn't get the amount</span>
<p className="max-w-52 text-center font-normal text-grey-1">
Ask the merchant to enter it and scan the QR again.
</p>
</Card>
<Button
onClick={() => {
setShowOrderNotReadyModal(false)
setShouldRetry(true)
}}
variant="purple"
shadowSize="4"
>
Scan the code again
</Button>
<button
onClick={() => setIsSupportModalOpen(true)}
className="flex w-full items-center justify-center gap-2 text-sm font-medium text-grey-1 transition-colors hover:text-black"
>
<Icon name="peanut-support" size={16} className="text-grey-1" />
Having trouble?
</button>
</div>
)
}
Expand Down
7 changes: 2 additions & 5 deletions src/components/Global/DirectSendQR/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,11 @@ export default function DirectSendQr({
icon = 'qr-code',
className = '',
ctaTitle,
iconClassName,
disabled = false,
}: {
className?: string
ctaTitle?: string
icon?: IconName
iconClassName?: string
disabled?: boolean
}) {
const [isQRScannerOpen, setIsQRScannerOpen] = useState(false)
Expand Down Expand Up @@ -407,13 +405,12 @@ export default function DirectSendQr({
shadowSize="4"
shadowType="primary"
className={twMerge(
'mx-auto h-20 w-20 cursor-pointer justify-center rounded-full p-0 hover:bg-primary-1/100',
'mx-auto h-20 w-20 cursor-pointer justify-center rounded-full p-3 hover:bg-primary-1/100',
className
)}
disabled={disabled}
>
<Icon name={icon} className={twMerge('custom-size', iconClassName)} />
{ctaTitle && ctaTitle}
<Icon name={icon} className="custom-size h-full" />
</Button>

<Modal
Expand Down
20 changes: 2 additions & 18 deletions src/components/Global/Icons/qr-code.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,10 @@
import { type FC, type SVGProps } from 'react'

export const QrCodeIcon: FC<SVGProps<SVGSVGElement>> = (props) => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<svg className={props.className} viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0.337891 10.6832H10.6854V0.335693H0.337891V10.6832ZM2.92477 2.92257H8.09852V8.09632H2.92477V2.92257Z"
d="M12 18H10V16H12V18ZM10 11H8V16H10V11ZM18 9H16V13H18V9ZM16 7H14V9H16V7ZM4 9H2V11H4V9ZM2 7H0V9H2V7ZM9 2H11V0H9V2ZM1.5 1.5V4.5H4.5V1.5H1.5ZM5 6H1C0.45 6 0 5.55 0 5V1C0 0.45 0.45 0 1 0H5C5.55 0 6 0.45 6 1V5C6 5.55 5.55 6 5 6ZM1.5 13.5V16.5H4.5V13.5H1.5ZM5 18H1C0.45 18 0 17.55 0 17V13C0 12.45 0.45 12 1 12H5C5.55 12 6 12.45 6 13V17C6 17.55 5.55 18 5 18ZM13.5 1.5V4.5H16.5V1.5H13.5ZM17 6H13C12.45 6 12 5.55 12 5V1C12 0.45 12.45 0 13 0H17C17.55 0 18 0.45 18 1V5C18 5.55 17.55 6 17 6ZM16 16V13H12V15H14V18H18V16H16ZM14 9H10V11H14V9ZM10 7H4V9H6V11H8V9H10V7ZM11 6V4H9V2H7V6H11ZM3.75 2.25H2.25V3.75H3.75V2.25ZM3.75 14.25H2.25V15.75H3.75V14.25ZM15.75 2.25H14.25V3.75H15.75V2.25Z"
Comment on lines +4 to +6
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 | 🟠 Major

A11Y and theming regression: props dropped; hardcoded black.

Removing props spread breaks aria/role propagation; fill="black" blocks theming via currentColor. Restore prop forwarding and use currentColor.

-export const QrCodeIcon: FC<SVGProps<SVGSVGElement>> = (props) => (
-    <svg className={props.className} viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+export const QrCodeIcon: FC<SVGProps<SVGSVGElement>> = ({ className, ...rest }) => (
+    <svg
+        className={className}
+        viewBox="0 0 18 18"
+        xmlns="http://www.w3.org/2000/svg"
+        aria-hidden={rest['aria-label'] ? undefined : true}
+        focusable="false"
+        {...rest}
+    >
         <path
-            d="M12 18H10V16H12V18ZM10 11H8V16H10V11ZM18 9H16V13H18V9ZM16 7H14V9H16V7ZM4 9H2V11H4V9ZM2 7H0V9H2V7ZM9 2H11V0H9V2ZM1.5 1.5V4.5H4.5V1.5H1.5ZM5 6H1C0.45 6 0 5.55 0 5V1C0 0.45 0.45 0 1 0H5C5.55 0 6 0.45 6 1V5C6 5.55 5.55 6 5 6ZM1.5 13.5V16.5H4.5V13.5H1.5ZM5 18H1C0.45 18 0 17.55 0 17V13C0 12.45 0.45 12 1 12H5C5.55 12 6 12.45 6 13V17C6 17.55 5.55 18 5 18ZM13.5 1.5V4.5H16.5V1.5H13.5ZM17 6H13C12.45 6 12 5.55 12 5V1C12 0.45 12.45 0 13 0H17C17.55 0 18 0.45 18 1V5C18 5.55 17.55 6 17 6ZM16 16V13H12V15H14V18H18V16H16ZM14 9H10V11H14V9ZM10 7H4V9H6V11H8V9H10V7ZM11 6V4H9V2H7V6H11ZM3.75 2.25H2.25V3.75H3.75V2.25ZM3.75 14.25H2.25V15.75H3.75V14.25ZM15.75 2.25H14.25V3.75H15.75V2.25Z"
-            fill="black"
+            d="M12 18H10V16H12V18ZM10 11H8V16H10V11ZM18 9H16V13H18V9ZM16 7H14V9H16V7ZM4 9H2V11H4V9ZM2 7H0V9H2V7ZM9 2H11V0H9V2ZM1.5 1.5V4.5H4.5V1.5H1.5ZM5 6H1C0.45 6 0 5.55 0 5V1C0 0.45 0.45 0 1 0H5C5.55 0 6 0.45 6 1V5C6 5.55 5.55 6 5 6ZM1.5 13.5V16.5H4.5V13.5H1.5ZM5 18H1C0.45 18 0 17.55 0 17V13C0 12.45 0.45 12 1 12H5C5.55 12 6 12.45 6 13V17C6 17.55 5.55 18 5 18ZM13.5 1.5V4.5H16.5V1.5H13.5ZM17 6H13C12.45 6 12 5.55 12 5V1C12 0.45 12.45 0 13 0H17C17.55 0 18 0.45 18 1V5C18 5.55 17.55 6 17 6ZM16 16V13H12V15H14V18H18V16H16ZM14 9H10V11H14V9ZM10 7H4V9H6V11H8V9H10V7ZM11 6V4H9V2H7V6H11ZM3.75 2.25H2.25V3.75H3.75V2.25ZM3.75 14.25H2.25V15.75H3.75V14.25ZM15.75 2.25H14.25V3.75H15.75V2.25Z"
+            fill="currentColor"
         />
     </svg>
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<svg className={props.className} viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0.337891 10.6832H10.6854V0.335693H0.337891V10.6832ZM2.92477 2.92257H8.09852V8.09632H2.92477V2.92257Z"
d="M12 18H10V16H12V18ZM10 11H8V16H10V11ZM18 9H16V13H18V9ZM16 7H14V9H16V7ZM4 9H2V11H4V9ZM2 7H0V9H2V7ZM9 2H11V0H9V2ZM1.5 1.5V4.5H4.5V1.5H1.5ZM5 6H1C0.45 6 0 5.55 0 5V1C0 0.45 0.45 0 1 0H5C5.55 0 6 0.45 6 1V5C6 5.55 5.55 6 5 6ZM1.5 13.5V16.5H4.5V13.5H1.5ZM5 18H1C0.45 18 0 17.55 0 17V13C0 12.45 0.45 12 1 12H5C5.55 12 6 12.45 6 13V17C6 17.55 5.55 18 5 18ZM13.5 1.5V4.5H16.5V1.5H13.5ZM17 6H13C12.45 6 12 5.55 12 5V1C12 0.45 12.45 0 13 0H17C17.55 0 18 0.45 18 1V5C18 5.55 17.55 6 17 6ZM16 16V13H12V15H14V18H18V16H16ZM14 9H10V11H14V9ZM10 7H4V9H6V11H8V9H10V7ZM11 6V4H9V2H7V6H11ZM3.75 2.25H2.25V3.75H3.75V2.25ZM3.75 14.25H2.25V15.75H3.75V14.25ZM15.75 2.25H14.25V3.75H15.75V2.25Z"
export const QrCodeIcon: FC<SVGProps<SVGSVGElement>> = ({ className, ...rest }) => (
<svg
className={className}
viewBox="0 0 18 18"
xmlns="http://www.w3.org/2000/svg"
aria-hidden={rest['aria-label'] ? undefined : true}
focusable="false"
{...rest}
>
<path
d="M12 18H10V16H12V18ZM10 11H8V16H10V11ZM18 9H16V13H18V9ZM16 7H14V9H16V7ZM4 9H2V11H4V9ZM2 7H0V9H2V7ZM9 2H11V0H9V2ZM1.5 1.5V4.5H4.5V1.5H1.5ZM5 6H1C0.45 6 0 5.55 0 5V1C0 0.45 0.45 0 1 0H5C5.55 0 6 0.45 6 1V5C6 5.55 5.55 6 5 6ZM1.5 13.5V16.5H4.5V13.5H1.5ZM5 18H1C0.45 18 0 17.55 0 17V13C0 12.45 0.45 12 1 12H5C5.55 12 6 12.45 6 13V17C6 17.55 5.55 18 5 18ZM13.5 1.5V4.5H16.5V1.5H13.5ZM17 6H13C12.45 6 12 5.55 12 5V1C12 0.45 12.45 0 13 0H17C17.55 0 18 0.45 18 1V5C18 5.55 17.55 6 17 6ZM16 16V13H12V15H14V18H18V16H16ZM14 9H10V11H14V9ZM10 7H4V9H6V11H8V9H10V7ZM11 6V4H9V2H7V6H11ZM3.75 2.25H2.25V3.75H3.75V2.25ZM3.75 14.25H2.25V15.75H3.75V14.25ZM15.75 2.25H14.25V3.75H15.75V2.25Z"
fill="currentColor"
/>
</svg>
)
🤖 Prompt for AI Agents
In src/components/Global/Icons/qr-code.tsx around lines 4 to 6, the SVG was
changed to drop prop forwarding and uses a hardcoded black fill which breaks
accessibility (aria/role) and theming; restore spreading incoming props onto the
root <svg> (e.g. keep className but add {...props}) so aria, role, title, etc.
propagate, and remove the hardcoded fill="black" on the path (or replace it with
fill="currentColor") so the icon respects CSS color/theming.

fill="black"
/>
<path
d="M0.337891 23.6176H10.6854V13.2701H0.337891V23.6176ZM2.92477 15.8569H8.09852V21.0307H2.92477V15.8569Z"
fill="black"
/>
<path
d="M13.2723 0.335693V10.6832H23.6198V0.335693H13.2723ZM21.0329 8.09632H15.8591V2.92257H21.0329V8.09632Z"
fill="black"
/>
<path d="M23.6198 21.0307H21.0329V23.6176H23.6198V21.0307Z" fill="black" />
<path d="M15.8591 13.2701H13.2723V15.8569H15.8591V13.2701Z" fill="black" />
<path d="M18.446 15.8569H15.8591V18.4438H18.446V15.8569Z" fill="black" />
<path d="M15.8591 18.4438H13.2723V21.0307H15.8591V18.4438Z" fill="black" />
<path d="M18.446 21.0307H15.8591V23.6176H18.446V21.0307Z" fill="black" />
<path d="M21.0329 18.4438H18.446V21.0307H21.0329V18.4438Z" fill="black" />
<path d="M21.0329 13.2701H18.446V15.8569H21.0329V13.2701Z" fill="black" />
<path d="M23.6198 15.8569H21.0329V18.4438H23.6198V15.8569Z" fill="black" />
</svg>
)
2 changes: 1 addition & 1 deletion src/components/Global/PeanutLoading/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function PeanutLoading({ coverFullScreen = false }: { coverFullSc
)}
>
<div className={twMerge('animate-spin')}>
<img src={PEANUTMAN_LOGO.src} alt="logo" className="h-6 sm:h-10" />
<img src={PEANUTMAN_LOGO.src} alt="logo" className="h-10" />
<span className="sr-only">Loading...</span>
</div>
</div>
Expand Down
49 changes: 0 additions & 49 deletions src/components/Home/RewardsCardModal.tsx

This file was deleted.

Loading