Skip to main content

WalletConnect Pay Integration Guide for Kotlin/Android

This guide enables Android wallet developers to integrate WalletConnect Pay into applications that already have WalletKit configured.By following this guide, your wallet will be able to process crypto payment links, allowing users to pay merchants directly from your app.

Before You Begin

Study First, Then Implement

This document is a reference guide, not boilerplate code to copy-paste. Before implementing:
  1. Study your existing codebase - Understand how URI handling, signing, and navigation work in your app
  2. Follow established patterns - Match your app’s architecture, naming conventions, and code style
  3. Adapt, don’t copy - The code examples show what to do, not necessarily how your specific app should do it

Prerequisites

  • WalletKit SDK integrated and initialized (com.reown:walletkit)
  • Ethereum signing capability (EIP-712 typed data, personal_sign)
  • Kotlin Coroutines for async operations
  • Understanding of your app’s navigation and state management

Dependencies

// In your build.gradle.kts
dependencies {
    implementation("com.reown:walletkit:$walletKitVersion")

    // For EIP-712 signing (if not already included)
    implementation("org.web3j:core:4.9.8")
}

Architecture Overview

How WalletConnect Pay Works

WalletConnect Pay enables crypto payments through payment links. The flow works as follows:
Payment Link → Get Options → Select Option → [WebView Data Collection]* → Sign Actions → Confirm Payment
       ↓             ↓            ↓                    ↓                        ↓              ↓
   Detection    API call    User picks       Per-option IC (if needed)     Wallet signs    Backend confirms

WalletKit Integration

WalletKit automatically initializes WalletConnectPay during WalletKit.initialize(). The Pay functionality is exposed through the WalletKit.Pay object:
object WalletKit {
    object Pay {
        suspend fun getPaymentOptions(paymentLink: String, accounts: List<String>): Result<PaymentOptionsResponse>
        suspend fun getRequiredPaymentActions(params: Params.RequiredPaymentActions): Result<List<RequiredAction>>
        suspend fun confirmPayment(params: Params.ConfirmPayment): Result<ConfirmPaymentResponse>
        fun isPaymentLink(uri: String): Boolean
    }
}

Using the Official Detection Function

WalletKit provides isPaymentLink() to identify payment URIs. Always use this function rather than custom URL parsing to ensure compatibility as the protocol evolves.
import com.reown.walletkit.client.WalletKit

fun handleUri(uri: String) {
    when {
        WalletKit.Pay.isPaymentLink(uri) -> {
            // Handle as payment link
            navigateToPaymentFlow(uri)
        }
        // Handle other URI types (WalletConnect pairing, deep links, etc.)
        else -> handleOtherUri(uri)
    }
}

Add Detection to All Entry Points

Payment link detection must be added wherever your app processes URIs:
  1. QR Code Scanner
fun onQrCodeScanned(scannedUri: String) {
    if (WalletKit.Pay.isPaymentLink(scannedUri)) {
        navigateToPaymentFlow(scannedUri)
    } else {
        // Handle as WalletConnect pairing or other URI
        handleWalletConnectUri(scannedUri)
    }
}
  1. Text Input / Paste
fun onUriPasted(pastedUri: String) {
    if (WalletKit.Pay.isPaymentLink(pastedUri)) {
        navigateToPaymentFlow(pastedUri)
    } else {
        handleGenericUri(pastedUri)
    }
}
  1. Deep Link Handler
// In your Activity or deep link handler
override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    intent?.data?.toString()?.let { uri ->
        if (WalletKit.Pay.isPaymentLink(uri)) {
            navigateToPaymentFlow(uri)
        } else {
            handleDeepLink(uri)
        }
    }
}
Important: The isPaymentLink() check must precede any generic HTTPS handlers to prevent payment links from accidentally opening in a browser.

Step 2: Implement the Payment Flow

Data Models

WalletKit exposes payment models under Wallet.Model:
// Payment status enumeration
enum class PaymentStatus {
    REQUIRES_ACTION,  // Additional action needed
    PROCESSING,       // Payment being processed
    SUCCEEDED,        // Payment completed
    FAILED,           // Payment failed
    EXPIRED           // Payment link expired
}

// Payment information from merchant
data class PaymentInfo(
    val status: PaymentStatus,
    val amount: PaymentAmount,
    val expiresAt: Long,
    val merchant: MerchantInfo
)

// Payment option (token/chain combination)
data class PaymentOption(
    val id: String,
    val amount: PaymentAmount,
    val account: String,
    val estimatedTxs: Int?,
    val collectData: CollectDataAction?  // Per-option data collection (null if not required)
)

// Data collection action (for KYC/compliance via WebView)
data class CollectDataAction(
    val url: String,           // WebView URL for data collection
    val schema: String?        // JSON schema describing required fields
)

// Transaction result details (present when payment already completed)
data class PaymentResultInfo(
    val txId: String,          // Transaction ID
    val optionAmount: PaymentAmount  // Token amount details
)

// Signing action required from wallet
data class WalletRpcAction(
    val chainId: String,
    val method: String,  // "eth_signTypedData_v4" or "personal_sign"
    val params: String   // JSON array as string
)

Payment Flow Implementation

2.1 Get Payment Options

import com.reown.walletkit.client.WalletKit
import com.reown.walletkit.client.Wallet

suspend fun getPaymentOptions(
    paymentLink: String,
    walletAddress: String
): Result<Wallet.Model.PaymentOptionsResponse> {
    // Format account as CAIP-10: "eip155:{chainId}:{address}"
    // Include all chains your wallet supports
    val accounts = listOf(
        "eip155:1:$walletAddress",     // Ethereum Mainnet
        "eip155:137:$walletAddress",   // Polygon
        "eip155:42161:$walletAddress", // Arbitrum
        "eip155:10:$walletAddress",    // Optimism
        "eip155:8453:$walletAddress"   // Base
    )

    return WalletKit.Pay.getPaymentOptions(
        paymentLink = paymentLink,
        accounts = accounts
    )
}

2.2 Handle Data Collection via WebView (if required)

Some payments require user information (KYC/AML compliance). Data collection requirements are specified per payment option via collectData. Show all options first, then handle data collection after the user selects an option:
fun processPaymentOptionsResponse(response: Wallet.Model.PaymentOptionsResponse) {
    if (response.options.isEmpty()) {
        showError("No payment options available")
        return
    }
    // Show all options — each option's collectData indicates if IC is needed
    showPaymentOptions(response.options, response.info)
}
When a selected option has collectData?.url, display the URL in a WebView before proceeding with signing. The hosted form handles rendering, validation, and Terms & Conditions acceptance. The WebView communicates completion via JavaScript bridge messages (IC_COMPLETE / IC_ERROR).
Important: When using the WebView approach, do not pass collectedData to confirmPayment(). The WebView submits data directly to the backend.

WebView Implementation

import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import android.webkit.WebResourceRequest
import android.content.Intent
import android.net.Uri
import android.util.Base64
import androidx.compose.runtime.Composable
import androidx.compose.ui.viewinterop.AndroidView
import org.json.JSONObject

@Composable
fun PayDataCollectionWebView(
    url: String,
    onComplete: () -> Unit,
    onError: (String) -> Unit
) {
    AndroidView(factory = { context ->
        WebView(context).apply {
            settings.javaScriptEnabled = true
            settings.domStorageEnabled = true
            settings.allowFileAccess = false

            addJavascriptInterface(
                object {
                    @JavascriptInterface
                    fun onDataCollectionComplete(json: String) {
                        val message = JSONObject(json)
                        when (message.optString("type")) {
                            "IC_COMPLETE" -> onComplete()
                            "IC_ERROR" -> onError(
                                message.optString("error", "Unknown error")
                            )
                        }
                    }
                },
                "AndroidWallet"
            )

            webViewClient = object : WebViewClient() {
                override fun shouldOverrideUrlLoading(
                    view: WebView?,
                    request: WebResourceRequest?
                ): Boolean {
                    val requestUrl = request?.url?.toString() ?: return false
                    if (!requestUrl.contains("pay.walletconnect.com")) {
                        context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(requestUrl)))
                        return true
                    }
                    return false
                }
            }

            loadUrl(url)
        }
    })
}

fun buildPrefillUrl(baseUrl: String, prefillData: Map<String, String>): String {
    if (prefillData.isEmpty()) return baseUrl
    val json = JSONObject(prefillData).toString()
    val base64 = Base64.encodeToString(
        json.toByteArray(),
        Base64.NO_WRAP or Base64.URL_SAFE
    )
    return Uri.parse(baseUrl).buildUpon()
        .appendQueryParameter("prefill", base64)
        .build().toString()
}

2.3 Get Required Payment Actions

After user selects a payment option, get the signing actions:
suspend fun getRequiredActions(
    paymentId: String,
    optionId: String
): Result<List<Wallet.Model.RequiredAction>> {
    return WalletKit.Pay.getRequiredPaymentActions(
        Wallet.Params.RequiredPaymentActions(
            paymentId = paymentId,
            optionId = optionId
        )
    )
}

2.4 Sign the Actions

Sign each WalletRpcAction with the wallet’s private key:
import org.json.JSONArray
import org.web3j.crypto.ECKeyPair
import org.web3j.crypto.Sign
import org.web3j.crypto.StructuredDataEncoder

fun signWalletRpcAction(
    action: Wallet.Model.WalletRpcAction,
    privateKey: ByteArray,
    walletAddress: String
): String {
    return when (action.method) {
        "eth_signTypedData_v4" -> signTypedDataV4(action.params, privateKey, walletAddress)
        "personal_sign" -> personalSign(action.params, privateKey)
        else -> throw UnsupportedOperationException("Unsupported method: ${action.method}")
    }
}

/**
 * Sign EIP-712 typed data.
 * The params are a JSON array: [address, typedDataJson]
 */
fun signTypedDataV4(
    params: String,
    privateKey: ByteArray,
    walletAddress: String
): String {
    val paramsArray = JSONArray(params)
    val requestedAddress = paramsArray.getString(0)
    val typedData = paramsArray.getString(1)

    // Verify address matches
    require(requestedAddress.equals(walletAddress, ignoreCase = true)) {
        "Requested address does not match wallet address"
    }

    // Hash the typed data using StructuredDataEncoder
    val encoder = StructuredDataEncoder(typedData)
    val hash = encoder.hashStructuredData()

    // Sign the hash
    val keyPair = ECKeyPair.create(privateKey)
    val signatureData = Sign.signMessage(hash, keyPair, false)

    // Encode signature as hex string
    val r = signatureData.r.toHexString()
    val s = signatureData.s.toHexString()
    val v = (signatureData.v[0].toInt() and 0xff).toString(16).padStart(2, '0')

    return "0x$r$s$v".lowercase()
}

/**
 * Sign a personal message.
 * The params are a JSON array: [messageHex, address]
 */
fun personalSign(
    params: String,
    privateKey: ByteArray
): String {
    val paramsArray = JSONArray(params)
    val messageHex = paramsArray.getString(0)

    // Remove "0x" prefix and decode hex to bytes
    val messageBytes = messageHex.removePrefix("0x").hexToByteArray()

    // Prefix with Ethereum signed message header
    val prefix = "\u0019Ethereum Signed Message:\n${messageBytes.size}"
    val prefixedMessage = prefix.toByteArray() + messageBytes

    // Hash and sign
    val hash = org.web3j.crypto.Hash.sha3(prefixedMessage)
    val keyPair = ECKeyPair.create(privateKey)
    val signatureData = Sign.signMessage(hash, keyPair, false)

    val r = signatureData.r.toHexString()
    val s = signatureData.s.toHexString()
    val v = (signatureData.v[0].toInt() and 0xff).toString(16).padStart(2, '0')

    return "0x$r$s$v".lowercase()
}

// Extension function for hex conversion
fun ByteArray.toHexString(): String = joinToString("") { "%02x".format(it) }
fun String.hexToByteArray(): ByteArray = chunked(2).map { it.toInt(16).toByte() }.toByteArray()
Critical: Pass raw JSON strings directly to signing APIs. Do not parse and reconstruct the JSON data, as this can cause transformation issues leading to signature verification failures.

2.5 Confirm Payment

Submit signatures and collected data to confirm the payment:
suspend fun confirmPayment(
    paymentId: String,
    optionId: String,
    signatures: List<String>
): Result<Wallet.Model.ConfirmPaymentResponse> {
    return WalletKit.Pay.confirmPayment(
        Wallet.Params.ConfirmPayment(
            paymentId = paymentId,
            optionId = optionId,
            signatures = signatures
        )
    )
}

// Handle the response
fun handleConfirmationResult(response: Wallet.Model.ConfirmPaymentResponse) {
    when (response.status) {
        Wallet.Model.PaymentStatus.SUCCEEDED -> {
            showSuccessScreen("Payment completed successfully!")
        }
        Wallet.Model.PaymentStatus.PROCESSING -> {
            // Payment is being processed asynchronously
            showSuccessScreen("Payment is being processed...")
            // Optionally poll for updates using response.pollInMs
        }
        Wallet.Model.PaymentStatus.FAILED -> {
            showErrorScreen("Payment failed. Please try again.")
        }
        Wallet.Model.PaymentStatus.EXPIRED -> {
            showErrorScreen("Payment link has expired.")
        }
        Wallet.Model.PaymentStatus.REQUIRES_ACTION -> {
            // Additional action needed (rare case)
            showErrorScreen("Additional action required.")
        }
    }
}

Step 3: State Management

Implement a state machine to manage the payment flow:
sealed class PaymentUiState {
    data object Loading : PaymentUiState()

    data class Options(
        val paymentInfo: Wallet.Model.PaymentInfo?,
        val options: List<Wallet.Model.PaymentOption>
    ) : PaymentUiState()

    data class WebViewDataCollection(
        val url: String
    ) : PaymentUiState()

    data class Processing(
        val message: String,
        val paymentInfo: Wallet.Model.PaymentInfo? = null
    ) : PaymentUiState()

    data class Success(
        val message: String,
        val paymentInfo: Wallet.Model.PaymentInfo? = null
    ) : PaymentUiState()

    data class Error(val message: String) : PaymentUiState()
}

ViewModel Implementation

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class PaymentViewModel(
    private val walletRepository: WalletRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow<PaymentUiState>(PaymentUiState.Loading)
    val uiState: StateFlow<PaymentUiState> = _uiState.asStateFlow()

    private var currentPaymentId: String? = null
    private var selectedOptionId: String? = null
    private var pendingActions: List<Wallet.Model.RequiredAction.WalletRpc> = emptyList()
    private var storedPaymentInfo: Wallet.Model.PaymentInfo? = null
    private var storedOptions: List<Wallet.Model.PaymentOption> = emptyList()

    fun loadPaymentOptions(paymentLink: String) {
        viewModelScope.launch {
            _uiState.value = PaymentUiState.Loading

            val accounts = walletRepository.getAccounts() // CAIP-10 format

            WalletKit.Pay.getPaymentOptions(paymentLink, accounts)
                .onSuccess { response ->
                    currentPaymentId = response.paymentId
                    storedPaymentInfo = response.info
                    storedOptions = response.options

                    if (response.options.isEmpty()) {
                        _uiState.value = PaymentUiState.Error("No payment options available")
                    } else {
                        _uiState.value = PaymentUiState.Options(
                            paymentInfo = response.info,
                            options = response.options
                        )
                    }
                }
                .onFailure { error ->
                    _uiState.value = PaymentUiState.Error(error.message ?: "Failed to load payment options")
                }
        }
    }

    fun selectPaymentOption(optionId: String) {
        val paymentId = currentPaymentId ?: return
        selectedOptionId = optionId

        // Check if the selected option requires data collection
        val selectedOption = storedOptions.find { it.id == optionId }
        selectedOption?.collectData?.url?.let { url ->
            // Show WebView for data collection before proceeding
            _uiState.value = PaymentUiState.WebViewDataCollection(url = url)
            return
        }

        // No data collection needed — proceed directly to signing
        proceedWithPaymentExecution(paymentId, optionId)
    }

    private fun proceedWithPaymentExecution(paymentId: String, optionId: String) {
        viewModelScope.launch {
            _uiState.value = PaymentUiState.Processing("Preparing payment...")

            WalletKit.Pay.getRequiredPaymentActions(
                Wallet.Params.RequiredPaymentActions(paymentId, optionId)
            )
                .onSuccess { actions ->
                    pendingActions = actions.filterIsInstance<Wallet.Model.RequiredAction.WalletRpc>()
                    executePayment()
                }
                .onFailure { error ->
                    _uiState.value = PaymentUiState.Error(error.message ?: "Failed to get payment actions")
                }
        }
    }

    private suspend fun executePayment() {
        val paymentId = currentPaymentId ?: return
        val optionId = selectedOptionId ?: return

        _uiState.value = PaymentUiState.Processing(
            message = "Confirming payment...",
            paymentInfo = storedPaymentInfo
        )

        try {
            // Sign all pending actions
            val signatures = pendingActions.map { action ->
                walletRepository.signWalletRpcAction(action.action)
            }

            // Confirm payment (no collectedData - WebView handles submission)
            WalletKit.Pay.confirmPayment(
                Wallet.Params.ConfirmPayment(
                    paymentId = paymentId,
                    optionId = optionId,
                    signatures = signatures
                )
            )
                .onSuccess { response ->
                    when (response.status) {
                        Wallet.Model.PaymentStatus.SUCCEEDED,
                        Wallet.Model.PaymentStatus.PROCESSING -> {
                            _uiState.value = PaymentUiState.Success(
                                message = if (response.status == Wallet.Model.PaymentStatus.SUCCEEDED)
                                    "Payment completed!" else "Payment processing...",
                                paymentInfo = storedPaymentInfo
                            )
                        }
                        else -> {
                            _uiState.value = PaymentUiState.Error("Payment ${response.status.name.lowercase()}")
                        }
                    }
                }
                .onFailure { error ->
                    _uiState.value = PaymentUiState.Error(error.message ?: "Payment confirmation failed")
                }
        } catch (e: Exception) {
            _uiState.value = PaymentUiState.Error(e.message ?: "An error occurred")
        }
    }

    fun onWebViewComplete() {
        // WebView data collection completed, proceed with payment execution for selected option
        val paymentId = currentPaymentId ?: return
        val optionId = selectedOptionId ?: return
        proceedWithPaymentExecution(paymentId, optionId)
    }

    fun onWebViewError(error: String) {
        _uiState.value = PaymentUiState.Error(error)
    }

    fun cancel() {
        currentPaymentId = null
        selectedOptionId = null
        pendingActions = emptyList()
        _uiState.value = PaymentUiState.Loading
    }
}

Step 4: UI Components

Required Screens

  1. Loading Screen - Show while fetching payment options
  2. Options Screen - List of payment options (tokens/chains). Show an “Info required” badge on options that have collectData != null
  3. Data Collection Screen - WebView for KYC/compliance data (shown after selecting an option that requires IC)
  4. Processing Screen - Show while signing and confirming
  5. Success Screen - Payment completed successfully
  6. Error Screen - Handle failures gracefully

Jetpack Compose Example

@Composable
fun PaymentScreen(
    viewModel: PaymentViewModel = viewModel(),
    onDismiss: () -> Unit
) {
    val uiState by viewModel.uiState.collectAsState()

    when (val state = uiState) {
        is PaymentUiState.Loading -> LoadingContent()

        is PaymentUiState.Options -> OptionsContent(
            paymentInfo = state.paymentInfo,
            options = state.options,
            onSelectOption = { option -> viewModel.selectPaymentOption(option.id) },
            onCancel = { viewModel.cancel(); onDismiss() }
        )

        is PaymentUiState.WebViewDataCollection -> PayDataCollectionWebView(
            url = state.url,
            onComplete = { viewModel.onWebViewComplete() },
            onError = { error -> viewModel.onWebViewError(error) }
        )

        is PaymentUiState.Processing -> ProcessingContent(
            message = state.message,
            paymentInfo = state.paymentInfo
        )

        is PaymentUiState.Success -> SuccessContent(
            message = state.message,
            paymentInfo = state.paymentInfo,
            onDone = { viewModel.cancel(); onDismiss() }
        )

        is PaymentUiState.Error -> ErrorContent(
            message = state.message,
            onRetry = { /* implement retry logic */ },
            onDismiss = { viewModel.cancel(); onDismiss() }
        )
    }
}

Utility Functions

Format Payment Amount

fun formatPaymentAmount(amount: Wallet.Model.PaymentAmount): String {
    val value = amount.value.toBigDecimalOrNull() ?: return "${amount.value} ${amount.unit}"
    val decimals = amount.display?.decimals ?: 18

    val divisor = BigDecimal.TEN.pow(decimals)
    val formattedValue = value.divide(divisor, decimals, RoundingMode.HALF_UP)
        .stripTrailingZeros()
        .toPlainString()

    val symbol = amount.display?.assetSymbol ?: amount.unit
    return "$formattedValue $symbol"
}

Format Expiry Time

fun formatExpiryTime(expiresAtMillis: Long): String {
    val now = System.currentTimeMillis()
    val remainingMillis = expiresAtMillis - now

    if (remainingMillis <= 0) return "Expired"

    val minutes = TimeUnit.MILLISECONDS.toMinutes(remainingMillis)
    val seconds = TimeUnit.MILLISECONDS.toSeconds(remainingMillis) % 60

    return when {
        minutes > 60 -> "${minutes / 60}h ${minutes % 60}m remaining"
        minutes > 0 -> "${minutes}m ${seconds}s remaining"
        else -> "${seconds}s remaining"
    }
}

Validate Date Input

fun validateDateInput(input: String): Boolean {
    // Expected format: YYYY-MM-DD
    val regex = Regex("""^\d{4}-\d{2}-\d{2}$""")
    if (!regex.matches(input)) return false

    return try {
        LocalDate.parse(input)
        true
    } catch (e: Exception) {
        false
    }
}

Error Handling

Common Errors

WalletKit exposes typed errors for payment operations:
sealed class PaymentError : Exception() {
    data class InvalidPaymentLink(override val message: String) : PaymentError()
    data class PaymentExpired(override val message: String) : PaymentError()
    data class PaymentNotFound(override val message: String) : PaymentError()
    data class InvalidRequest(override val message: String) : PaymentError()
    data class ComplianceFailed(override val message: String) : PaymentError()
    data class NetworkError(override val message: String) : PaymentError()
    data class InternalError(override val message: String) : PaymentError()
}

Error Handling Strategy

fun handlePaymentError(error: Throwable): String {
    return when (error) {
        is PaymentError.InvalidPaymentLink -> "Invalid payment link format"
        is PaymentError.PaymentExpired -> "This payment link has expired"
        is PaymentError.PaymentNotFound -> "Payment not found"
        is PaymentError.ComplianceFailed -> "Unable to process payment due to compliance requirements"
        is PaymentError.NetworkError -> "Network error. Please check your connection"
        else -> error.message ?: "An unexpected error occurred"
    }
}

Troubleshooting

Payment Options Empty

Symptom: getPaymentOptions returns an empty options list. Causes:
  • Wallet address not supported by merchant
  • Chain not supported for this payment
  • Payment link already used or expired
Solution:
  • Verify CAIP-10 account format: eip155:{chainId}:{address}
  • Include all chains your wallet supports
  • Check if payment link is still valid

Signature Verification Failed

Symptom: confirmPayment fails with signature verification error. Causes:
  • Incorrect EIP-712 signing implementation
  • JSON transformation altering the typed data
  • Wrong address used for signing
Solution:
  • Use StructuredDataEncoder for EIP-712 hashing
  • Pass raw JSON strings to signing without parsing/reconstruction
  • Verify the signing address matches the requested address in params

Wrong Chain

Symptom: Transaction fails or signing produces unexpected results. Causes:
  • Not using the chainId from WalletRpcAction
  • Signing with wrong network context
Solution:
  • Always use action.chainId to determine which network to sign for
  • Ensure your wallet’s signing context matches the action’s chain

Payment Status Polling

For PROCESSING status, implement polling:
suspend fun pollPaymentStatus(
    paymentId: String,
    optionId: String,
    pollIntervalMs: Long
) {
    delay(pollIntervalMs)
    // Re-call getPaymentOptions or implement a status check endpoint
    // Continue polling until status is final (SUCCEEDED, FAILED, EXPIRED)
}

Complete Flow Example

class PaymentFlowExample(
    private val walletRepository: WalletRepository
) {
    suspend fun processPayment(paymentLink: String): Result<Unit> = runCatching {
        // 1. Get accounts in CAIP-10 format
        val accounts = walletRepository.getAccountsAsCaip10()

        // 2. Get payment options
        val optionsResponse = WalletKit.Pay.getPaymentOptions(paymentLink, accounts)
            .getOrThrow()

        val paymentId = optionsResponse.paymentId

        // 3. Let user select payment option
        val selectedOption = showOptionsAndGetSelection(optionsResponse.options)

        // 4. Handle data collection via WebView if required for selected option
        selectedOption.collectData?.url?.let { url ->
            showDataCollectionWebView(url)
            // Wait for IC_COMPLETE before proceeding
        }

        // 5. Get required signing actions
        val actions = WalletKit.Pay.getRequiredPaymentActions(
            Wallet.Params.RequiredPaymentActions(paymentId, selectedOption.id)
        ).getOrThrow()

        // 6. Sign all actions
        val signatures = actions
            .filterIsInstance<Wallet.Model.RequiredAction.WalletRpc>()
            .map { walletRepository.signWalletRpcAction(it.action) }

        // 7. Confirm payment
        val confirmResponse = WalletKit.Pay.confirmPayment(
            Wallet.Params.ConfirmPayment(
                paymentId = paymentId,
                optionId = selectedOption.id,
                signatures = signatures
            )
        ).getOrThrow()

        // 8. Handle result
        when (confirmResponse.status) {
            Wallet.Model.PaymentStatus.SUCCEEDED -> { /* Success! */ }
            Wallet.Model.PaymentStatus.PROCESSING -> {
                // Optionally poll for final status
                confirmResponse.pollInMs?.let { delay(it) }
            }
            else -> throw Exception("Payment ${confirmResponse.status}")
        }
    }
}

Reference Implementation

For a complete working example, see the sample wallet implementation:
  • sample/wallet/src/main/kotlin/com/reown/sample/wallet/ui/routes/dialog_routes/payment/PaymentViewModel.kt
  • sample/wallet/src/main/kotlin/com/reown/sample/wallet/ui/routes/dialog_routes/payment/PaymentRoute.kt

Additional Resources