AI Integration Prompt - Kotlin - WalletConnect Pay Docs

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

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]* → Get Actions → Sign Actions → Confirm Payment
       ↓             ↓            ↓                    ↓                         ↓             ↓              ↓
   Detection    API call    User picks       Per-option IC (if needed)    Fetch actions  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
    }
}

Step 1: Payment Link Detection

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)
        }
    }
}

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 selectedOption.collectData?.url is present, 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). The form URL accepts optional query parameters (append them with buildFormUrl before loading):

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()

2.5 Confirm Payment

Submit the signatures to confirm the payment (data collection, if any, was already submitted by the WebView):

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

Recommended State Machine

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...",