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:
- Study your existing codebase - Understand how URI handling, signing, and navigation work in your app
- Follow established patterns - Match your app’s architecture, naming conventions, and code style
- 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]* → 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:
- QR Code Scanner
fun onQrCodeScanned(scannedUri: String) {
if (WalletKit.Pay.isPaymentLink(scannedUri)) {
navigateToPaymentFlow(scannedUri)
} else {
// Handle as WalletConnect pairing or other URI
handleWalletConnectUri(scannedUri)
}
}
- Text Input / Paste
fun onUriPasted(pastedUri: String) {
if (WalletKit.Pay.isPaymentLink(pastedUri)) {
navigateToPaymentFlow(pastedUri)
} else {
handleGenericUri(pastedUri)
}
}
- 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):
prefill— base64url-encoded JSON of known user fields. Keys must match therequiredfields fromcollectData.schema(e.g.fullName,dob,pobAddress).theme—lightordark, to match your wallet’s color mode.themeVariables— a base64url string exported from the WalletConnect Pay Dashboard that overrides design tokens (font, font size, some colors, button/input border radius). Append it verbatim.
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...",