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
16 changes: 10 additions & 6 deletions src/app/[...recipient]/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ import ActionList from '@/components/Common/ActionList'
import NavHeader from '@/components/Global/NavHeader'
import { ReqFulfillBankFlowManager } from '@/components/Request/views/ReqFulfillBankFlowManager'

export type PaymentFlow = 'request_pay' | 'external_wallet' | 'direct_pay' | 'withdraw'
interface Props {
recipient: string[]
flow?: 'request_pay' | 'external_wallet' | 'direct_pay' | 'withdraw'
flow?: PaymentFlow
}

export default function PaymentPage({ recipient, flow = 'request_pay' }: Props) {
Expand Down Expand Up @@ -455,6 +456,7 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props)
{currentView === 'INITIAL' && (
<div className="space-y-2">
<InitialPaymentView
flow={flow}
key={`initial-${flow}`}
{...(parsedPaymentData as ParsedURL)}
isExternalWalletFlow={isExternalWalletFlow}
Expand All @@ -472,11 +474,13 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props)
currencyAmount={currencyAmount}
/>
<div>
<ActionList
flow="request"
requestLinkData={parsedPaymentData as ParsedURL}
isLoggedIn={!!user?.user.userId}
/>
{flow !== 'direct_pay' && (
<ActionList
flow="request"
requestLinkData={parsedPaymentData as ParsedURL}
isLoggedIn={!!user?.user.userId}
/>
)}
</div>
</div>
)}
Expand Down
78 changes: 72 additions & 6 deletions src/components/Global/SoundPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useEffect } from 'react'
import { useEffect, useRef } from 'react'

const soundMap = {
success: '/sounds/success.mp3',
Expand All @@ -16,13 +16,79 @@ type SoundPlayerProps = {
* @returns null
*/
export const SoundPlayer = ({ sound }: SoundPlayerProps) => {
const audioRef = useRef<HTMLAudioElement | null>(null)

useEffect(() => {
const audioSrc = soundMap[sound]
if (audioSrc) {
const audio = new Audio(audioSrc)
audio.play().catch((error) => {
console.error(`Audio play failed for sound "${sound}":`, error)
})
if (!audioSrc) return

// create audio element and prepare to play, hint browser to load audio early, keep a ref so we can clean up later
const audio = new Audio(audioSrc)
audio.preload = 'auto'
audioRef.current = audio

// track if we attached one-time unlock listeners and the unlock handler
let listenersAdded = false
let unlock: (() => void) | null = null

// clean up any unlock listeners if they were added
const removeListeners = () => {
if (!listenersAdded || !unlock) return
document.removeEventListener('pointerdown', unlock)
document.removeEventListener('touchstart', unlock)
document.removeEventListener('click', unlock)
listenersAdded = false
unlock = null
}

// try to play immediately, if blocked by autoplay policies, wait for user interaction
const tryPlay = () =>
audio
.play()
.then(() => {
// success, nothing else to do
})
.catch((error: any) => {
// detect common autoplay restriction errors
const message = String(error?.message || '')
const isAutoplayBlocked =
error?.name === 'NotAllowedError' || /gesture|autoplay|play\(\) failed/i.test(message)

if (isAutoplayBlocked) {
// defer playback until next user interaction (mobile/pwa autoplay policies)
unlock = () => {
audio
.play()
.catch((e) => {
console.error(`Audio play failed after user interaction for sound "${sound}":`, e)
})
.finally(() => {
removeListeners()
})
}
// attach one-time listeners so the first tap/click resumes playback
document.addEventListener('pointerdown', unlock, { once: true })
document.addEventListener('touchstart', unlock, { once: true })
document.addEventListener('click', unlock, { once: true })
listenersAdded = true
} else {
console.error(`Audio play failed for sound "${sound}":`, error)
}
})

tryPlay()

return () => {
// on unmount, remove listeners and stop audio if needed
removeListeners()
if (audioRef.current) {
try {
audioRef.current.pause()
} catch (_) {
// ignore
}
}
audioRef.current = null
}
}, [sound])

Expand Down
23 changes: 7 additions & 16 deletions src/components/Payment/PaymentForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,7 @@ import TokenAmountInput from '@/components/Global/TokenAmountInput'
import BeerInput from '@/components/PintaReqPay/BeerInput'
import PintaReqViewWrapper from '@/components/PintaReqPay/PintaReqViewWrapper'
import UserCard from '@/components/User/UserCard'
import {
PEANUT_WALLET_CHAIN,
PEANUT_WALLET_TOKEN,
PEANUT_WALLET_TOKEN_DECIMALS,
PEANUT_WALLET_TOKEN_SYMBOL,
PINTA_WALLET_CHAIN,
PINTA_WALLET_TOKEN,
} from '@/constants'
import { PEANUT_WALLET_TOKEN, PEANUT_WALLET_TOKEN_DECIMALS, PINTA_WALLET_CHAIN, PINTA_WALLET_TOKEN } from '@/constants'
import { tokenSelectorContext } from '@/context'
import { useAuth } from '@/context/authContext'
import { useRequestFulfillmentFlow } from '@/context/RequestFulfillmentFlowContext'
Expand All @@ -35,7 +28,6 @@ import { useAppDispatch, usePaymentStore } from '@/redux/hooks'
import { paymentActions } from '@/redux/slices/payment-slice'
import { walletActions } from '@/redux/slices/wallet-slice'
import { areEvmAddressesEqual, ErrorHandler, formatAmount } from '@/utils'
import { useDaimoPayUI } from '@daimo/pay'
import { useAppKit, useDisconnect } from '@reown/appkit/react'
import Image from 'next/image'
import { useRouter, useSearchParams } from 'next/navigation'
Expand All @@ -44,6 +36,7 @@ import { formatUnits } from 'viem'
import { useAccount } from 'wagmi'
import { useUserInteractions } from '@/hooks/useUserInteractions'
import { useUserByUsername } from '@/hooks/useUserByUsername'
import { PaymentFlow } from '@/app/[...recipient]/client'

export type PaymentFlowProps = {
isPintaReq?: boolean
Expand All @@ -58,6 +51,7 @@ export type PaymentFlowProps = {
currencyAmount?: string
setCurrencyAmount?: (currencyvalue: string | undefined) => void
headerTitle?: string
flow?: PaymentFlow
}

export type PaymentFormProps = ParsedURL & PaymentFlowProps
Expand All @@ -74,6 +68,7 @@ export const PaymentForm = ({
isExternalWalletFlow,
isDirectUsdPayment,
headerTitle,
flow,
}: PaymentFormProps) => {
const dispatch = useAppDispatch()
const router = useRouter()
Expand Down Expand Up @@ -632,6 +627,7 @@ export const PaymentForm = ({
}

const daimoButton = () => {
if (flow === 'direct_pay') return null
return (
<DaimoPayButton
amount={inputTokenAmount}
Expand Down Expand Up @@ -705,6 +701,8 @@ export const PaymentForm = ({
)
}

if (flow === 'request_pay') return false

// logic for non-AddMoneyFlow / non-Pinta (Pinta has its own button logic)
if (!isPintaReq) {
if (!isConnected) return true // if not connected at all, disable (covers guest non-Peanut scenarios)
Expand Down Expand Up @@ -758,13 +756,6 @@ export const PaymentForm = ({
return recipient ? recipient.identifier : 'Unknown Recipient'
}, [recipient])

const isPeanutWalletUSDC = useMemo(() => {
return (
selectedTokenData?.symbol === PEANUT_WALLET_TOKEN_SYMBOL &&
Number(selectedChainID) === PEANUT_WALLET_CHAIN.id
)
}, [selectedTokenData, selectedChainID])

const handleGoBack = () => {
if (isExternalWalletFlow) {
setShowExternalWalletFulfilMethods(true)
Expand Down
Loading