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
5 changes: 5 additions & 0 deletions .changeset/cyan-nails-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@reown/appkit-pay': patch
---

Add new pay method without the need to subscribe to events
49 changes: 15 additions & 34 deletions apps/laboratory/src/components/AppKitPay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,12 @@ import {
import { Card } from '@chakra-ui/react'

import type { CaipNetworkId } from '@reown/appkit-common'
import type {
AppKitPayErrorMessage,
Exchange,
PayResult,
PayUrlParams,
PaymentAsset
} from '@reown/appkit-pay'
import { baseETH, baseSepoliaETH, baseUSDC } from '@reown/appkit-pay'
import type { Exchange, PayUrlParams, PaymentAsset } from '@reown/appkit-pay'
import { baseETH, baseSepoliaETH, baseUSDC, pay } from '@reown/appkit-pay'
import {
type ExchangeBuyStatus,
useAvailableExchanges,
useExchangeBuyStatus,
usePay,
usePayUrlActions
} from '@reown/appkit-pay/react'
import { solana, solanaDevnet } from '@reown/appkit/networks'
Expand Down Expand Up @@ -102,27 +95,6 @@ export function AppKitPay() {
const { isOpen, onToggle } = useDisclosure()
const toast = useChakraToast()

function handleSuccess(resultData: PayResult) {
toast({
title: 'Transaction successful',
description: resultData,
type: 'success'
})
}

function handleError(errorData: AppKitPayErrorMessage) {
toast({
title: 'Transaction failed',
description: errorData,
type: 'error'
})
}

const { open, isPending } = usePay({
onSuccess: handleSuccess,
onError: handleError
})

const {
data: fetchedExchangesData,
isLoading: isLoadingExchanges,
Expand Down Expand Up @@ -215,12 +187,21 @@ export function AppKitPay() {

return
}

await open({
const result = await pay({
recipient: paymentDetails.recipient,
amount: paymentDetails.amount,
paymentAsset: paymentDetails.asset
})

if (result.success) {
toast({ title: 'Payment Succeeded', description: `Tx: ${result.result}`, type: 'success' })
} else {
toast({
title: 'Payment Failed',
description: result.error ?? 'Unknown error',
type: 'error'
})
}
}

const handleInputChange = useCallback(
Expand Down Expand Up @@ -518,8 +499,8 @@ export function AppKitPay() {
</CardHeader>
<CardBody>
<Stack spacing="4">
<Button onClick={handleOpenPay} isDisabled={isPending} width="full">
{isPending ? <Spinner /> : 'Open Pay Modal'}
<Button onClick={handleOpenPay} width="full">
Open Pay Modal
</Button>

<Text fontSize="sm" color="gray.500">
Expand Down
3 changes: 2 additions & 1 deletion packages/pay/exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export {
getExchanges,
getPayResult,
getPayError,
getIsPaymentInProgress
getIsPaymentInProgress,
pay
} from '../src/client.js'

// -- Types ----------------------------------------- //
Expand Down
110 changes: 110 additions & 0 deletions packages/pay/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,108 @@
import { PayController, type PayControllerState } from './controllers/PayController.js'
import { AppKitPayError, AppKitPayErrorCodes } from './types/errors.js'
import type { GetExchangesParams, PayUrlParams, PaymentOptions } from './types/options.js'
import type { PaymentResult } from './types/payment.js'

// 5 minutes
const PAYMENT_TIMEOUT_MS = 300000

export async function openPay(options: PaymentOptions) {
return PayController.handleOpenPay(options)
}

export async function pay(
options: PaymentOptions,
timeoutMs = PAYMENT_TIMEOUT_MS
): Promise<PaymentResult> {
if (timeoutMs <= 0) {
throw new AppKitPayError(
AppKitPayErrorCodes.INVALID_PAYMENT_CONFIG,
'Timeout must be greater than 0'
)
}

try {
await openPay(options)
} catch (error) {
if (error instanceof AppKitPayError) {
throw error
}
throw new AppKitPayError(
AppKitPayErrorCodes.UNABLE_TO_INITIATE_PAYMENT,
(error as Error).message
)
}
Comment thread
lukaisailovic marked this conversation as resolved.

return new Promise<PaymentResult>((resolve, reject) => {
let isSettled = false

const timeoutId = setTimeout(() => {
if (isSettled) {
return
}
isSettled = true
cleanup()
reject(new AppKitPayError(AppKitPayErrorCodes.GENERIC_PAYMENT_ERROR, 'Payment timeout'))
}, timeoutMs)

function checkAndResolve(): void {
if (isSettled) {
return
}

const currentPayment = PayController.state.currentPayment
const error = PayController.state.error
const isInProgress = PayController.state.isPaymentInProgress

if (currentPayment?.status === 'SUCCESS') {
isSettled = true
cleanup()
clearTimeout(timeoutId)
resolve({
success: true,
result: currentPayment.result
})

return
}

if (currentPayment?.status === 'FAILED') {
isSettled = true
cleanup()
clearTimeout(timeoutId)
resolve({
success: false,
error: error || 'Payment failed'
})

return
}

if (error && !isInProgress && !currentPayment) {
isSettled = true
cleanup()
clearTimeout(timeoutId)
resolve({
success: false,
error
})
}
}

const unsubscribePayment = subscribeStateKey('currentPayment', checkAndResolve)
const unsubscribeError = subscribeStateKey('error', checkAndResolve)
const unsubscribeProgress = subscribeStateKey('isPaymentInProgress', checkAndResolve)

const cleanup = createCleanupHandler([
unsubscribePayment,
unsubscribeError,
unsubscribeProgress
])

checkAndResolve()
})
}

export function getAvailableExchanges(params?: GetExchangesParams) {
return PayController.getAvailableExchanges(params)
}
Expand Down Expand Up @@ -44,3 +142,15 @@ export function subscribeStateKey<K extends keyof PayControllerPublicState>(
) {
return PayController.subscribeKey(key, callback as (value: PayControllerState[K]) => void)
}

function createCleanupHandler(unsubscribeFunctions: Array<() => void>) {
return (): void => {
unsubscribeFunctions.forEach(unsubscribe => {
try {
unsubscribe()
} catch {
// Ignore cleanup errors
}
})
}
}
30 changes: 21 additions & 9 deletions packages/pay/src/controllers/PayController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { type Address, ConstantsUtil, ParseUtil } from '@reown/appkit-common'
import {
AccountController,
ChainController,
ConnectionController,
CoreHelperUtil,
EventsController,
ModalController,
Expand Down Expand Up @@ -296,19 +297,27 @@ export const PayController = {
if (state.isConfigured) {
return
}
ProviderUtil.subscribeProviders(async _ => {
const provider = ProviderUtil.getProvider(ChainController.state.activeChain)
if (!provider) {
return

ConnectionController.subscribeKey('connections', connections => {
if (connections.size > 0) {
this.handlePayment()
}
await this.handlePayment()
})

AccountController.subscribeKey('caipAddress', async caipAddress => {
if (!caipAddress) {
return
AccountController.subscribeKey('caipAddress', caipAddress => {
const hasWcConnection = ConnectionController.hasAnyConnection(
ConstantsUtil.CONNECTOR_ID.WALLET_CONNECT
)
if (caipAddress) {
// WalletConnect connections sometimes fail down the line due to state not being updated atomically
if (hasWcConnection) {
setTimeout(() => {
this.handlePayment()
}, 100)
} else {
this.handlePayment()
}
}
await this.handlePayment()
})
},
async handlePayment() {
Expand All @@ -323,16 +332,19 @@ export const PayController = {

const { chainId, address } = ParseUtil.parseCaipAddress(caipAddress)
const chainNamespace = ChainController.state.activeChain

if (!address || !chainId || !chainNamespace) {
return
}

const provider = ProviderUtil.getProvider(chainNamespace)

if (!provider) {
return
}

const caipNetwork = ChainController.state.activeCaipNetwork

if (!caipNetwork) {
return
}
Expand Down
5 changes: 5 additions & 0 deletions packages/pay/src/types/payment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type PaymentResult = {
success: boolean
result?: string
error?: string
}