Overview
WalletConnect integration lets your end-users connect their Dynamic embedded wallet to any WalletConnect-compatible dApp. Users scan a QR code (or paste a URI) from a dApp, approve the session, and can then sign transactions and messages directly from your app. This turns your app into a full-featured wallet that works across the web3 ecosystem.This feature is currently restricted to EVM embedded wallets. Solana support may be added as more dApps adopt WalletConnect for Solana.
Prerequisites
- Dynamic SDK initialized with a Reown Project ID (see Quickstart)
- User authenticated (see Authentication)
- An EVM embedded wallet created (see Wallet Creation)
- Camera permission in
AndroidManifest.xmlfor QR scanning:
<uses-permission android:name="android.permission.CAMERA" />
- QR scanning dependencies in
build.gradle.kts:
// CameraX for camera preview
implementation("androidx.camera:camera-core:1.3.1")
implementation("androidx.camera:camera-camera2:1.3.1")
implementation("androidx.camera:camera-lifecycle:1.3.1")
implementation("androidx.camera:camera-view:1.3.1")
// ML Kit Barcode Scanning
implementation("com.google.mlkit:barcode-scanning:17.2.0")
How It Works
- User opens a dApp on desktop or another device and clicks “Connect Wallet”
- The dApp shows a WalletConnect QR code
- User scans the QR code (or pastes the
wc:URI) in your app - Your app shows a session proposal with the dApp’s details and requested chains
- User approves or rejects the connection
- Once connected, signing requests from the dApp appear as approval dialogs in your app
- User approves or rejects each request
Setup
1. Configure the Reown Project ID
Pass your Reown (formerly WalletConnect) project ID when initializing the SDK. Get one at cloud.reown.com.import com.dynamic.sdk.android.DynamicSDK
import com.dynamic.sdk.android.Models.ClientProps
import com.dynamic.sdk.android.Models.LoggerLevel
val props = ClientProps(
environmentId = "YOUR_ENVIRONMENT_ID",
appName = "My App",
appLogoUrl = "https://example.com/logo.png",
redirectUrl = "myapp://",
appOrigin = "https://example.com",
apiBaseUrl = "https://app.dynamicauth.com/api/v0",
// Required for WalletConnect
reownProjectId = "YOUR_REOWN_PROJECT_ID"
)
DynamicSDK.initialize(props, applicationContext, activity)
2. Add the Global Listener
TheWcGlobalListener is a Composable that handles the entire WalletConnect lifecycle: initialization, session proposals, signing requests, and disconnection events. Place it at the root of your app so it can show approval dialogs from any screen.
import com.dynamic.sdk.android.DynamicSDK
@Composable
fun App() {
WcGlobalListener {
// Your app's root content
AppNavigation()
}
}
3. Implement the WcGlobalListener
This is the full implementation. It auto-initializes WC when the user logs in and shows dialogs for session proposals and signing requests.import android.widget.Toast
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.dynamic.sdk.android.DynamicSDK
import com.dynamic.sdk.android.Models.WcSessionProposal
import com.dynamic.sdk.android.Models.WcSessionRequest
import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonElement
@Composable
fun WcGlobalListener(content: @Composable () -> Unit) {
val sdk = DynamicSDK.getInstance()
val wc = sdk.walletConnect
val scope = rememberCoroutineScope()
val context = LocalContext.current
var initialized by remember { mutableStateOf(false) }
var initializing by remember { mutableStateOf(false) }
// Dialog state
var showProposal by remember {
mutableStateOf<WcSessionProposal?>(null)
}
var showRequest by remember {
mutableStateOf<WcSessionRequest?>(null)
}
val snackbarHostState = remember { SnackbarHostState() }
// Wait for auth token AND WC webview initialization
val token by sdk.auth.tokenChanges.collectAsState()
val wcInitialized by wc.initializedChanges.collectAsState()
LaunchedEffect(token, wcInitialized) {
if (
!token.isNullOrEmpty()
&& wcInitialized
&& !initialized
&& !initializing
) {
initializing = true
try {
wc.initialize()
initialized = true
} catch (e: Exception) {
android.util.Log.e(
"WcGlobalListener",
"Failed to initialize WC: ${e.message}"
)
}
initializing = false
} else if (token.isNullOrEmpty()) {
initialized = false
initializing = false
}
}
// Listen for WC events
LaunchedEffect(Unit) {
launch {
wc.onSessionProposal.collect { proposal ->
showProposal = proposal
}
}
launch {
wc.onSessionRequest.collect { request ->
showRequest = request
}
}
launch {
wc.onSessionDelete.collect { topic ->
snackbarHostState.showSnackbar(
"Session disconnected: ${topic?.take(8)}..."
)
}
}
}
Box(modifier = Modifier.fillMaxSize()) {
content()
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
// Session Proposal Dialog
showProposal?.let { proposal ->
ProposalDialog(
proposal = proposal,
onApprove = {
showProposal = null
scope.launch {
try {
wc.confirmPairing(true)
snackbarHostState.showSnackbar(
"Session approved"
)
} catch (e: Exception) {
snackbarHostState.showSnackbar(
"Approval failed: ${e.message}"
)
}
}
},
onReject = {
showProposal = null
scope.launch {
try { wc.confirmPairing(false) }
catch (_: Exception) {}
}
}
)
}
// Session Request Dialog
showRequest?.let { request ->
RequestDialog(
request = request,
onApprove = {
showRequest = null
scope.launch {
try {
wc.respondSessionRequest(
request.id,
request.topic,
true
)
} catch (e: Exception) {
snackbarHostState.showSnackbar(
"Request failed: ${e.message}"
)
}
}
},
onReject = {
showRequest = null
scope.launch {
try {
wc.respondSessionRequest(
request.id,
request.topic,
false
)
} catch (_: Exception) {}
}
}
)
}
}
@Composable
private fun ProposalDialog(
proposal: WcSessionProposal,
onApprove: () -> Unit,
onReject: () -> Unit
) {
AlertDialog(
onDismissRequest = { /* non-dismissible */ },
title = { Text("Session Proposal") },
text = {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
) {
Text(
text = proposal.proposer.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
if (proposal.proposer.description.isNotEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = proposal.proposer.description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme
.onSurfaceVariant
)
}
if (proposal.proposer.url.isNotEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = proposal.proposer.url,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
fontSize = 12.sp
)
}
Spacer(modifier = Modifier.height(16.dp))
proposal.requiredNamespaces?.let { namespaces ->
Text(
"Required chains:",
fontWeight = FontWeight.SemiBold,
fontSize = 13.sp
)
namespaces.forEach { (key, ns) ->
Text(
" $key: ${ns.chains.joinToString(", ")}",
fontSize = 13.sp
)
}
}
proposal.optionalNamespaces
?.takeIf { it.isNotEmpty() }
?.let { namespaces ->
Spacer(modifier = Modifier.height(8.dp))
Text(
"Optional chains:",
fontWeight = FontWeight.SemiBold,
fontSize = 13.sp
)
namespaces.forEach { (key, ns) ->
Text(
" $key: ${ns.chains.joinToString(", ")}",
fontSize = 13.sp
)
}
}
}
},
confirmButton = {
Button(onClick = onApprove) { Text("Approve") }
},
dismissButton = {
TextButton(onClick = onReject) { Text("Reject") }
}
)
}
@Composable
private fun RequestDialog(
request: WcSessionRequest,
onApprove: () -> Unit,
onReject: () -> Unit
) {
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
AlertDialog(
onDismissRequest = { /* non-dismissible */ },
title = { Text("Session Request") },
text = {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
) {
InfoRow(label = "Method", value = request.method)
InfoRow(label = "Chain", value = request.chainId)
InfoRow(
label = "Topic",
value = "${request.topic.take(12)}..."
)
request.params?.let { params ->
Spacer(modifier = Modifier.height(8.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Params:",
fontWeight = FontWeight.SemiBold,
fontSize = 13.sp
)
Spacer(modifier = Modifier.weight(1f))
IconButton(
onClick = {
clipboardManager.setText(
AnnotatedString(params.toString())
)
Toast.makeText(
context,
"Params copied",
Toast.LENGTH_SHORT
).show()
},
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Default.ContentCopy,
contentDescription = "Copy",
modifier = Modifier.size(16.dp)
)
}
}
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme
.surfaceVariant,
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 200.dp)
) {
Box(
modifier = Modifier
.padding(8.dp)
.verticalScroll(rememberScrollState())
) {
Text(
text = params.toString(),
fontFamily = FontFamily.Monospace,
fontSize = 11.sp
)
}
}
}
}
},
confirmButton = {
Button(onClick = onApprove) { Text("Approve") }
},
dismissButton = {
TextButton(onClick = onReject) { Text("Reject") }
}
)
}
@Composable
private fun InfoRow(label: String, value: String) {
Row(modifier = Modifier.padding(bottom = 4.dp)) {
Text(
"$label:",
fontWeight = FontWeight.SemiBold,
fontSize = 13.sp,
modifier = Modifier.width(60.dp)
)
Text(value, fontSize = 13.sp)
}
}
4. Build the WalletConnect Screen
This screen provides the UI for scanning QR codes, pasting URIs, and managing active sessions.import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.LinkOff
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.dynamic.sdk.android.DynamicSDK
import com.dynamic.sdk.android.Models.WcSession
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WalletConnectScreen(onNavigateBack: () -> Unit) {
val wc = DynamicSDK.getInstance().walletConnect
val scope = rememberCoroutineScope()
var uriText by remember { mutableStateOf("") }
var isPairing by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<String?>(null) }
var showScanner by remember { mutableStateOf(false) }
val sessions by wc.sessionsChanges.collectAsState()
fun doPair(uri: String) {
scope.launch {
isPairing = true
error = null
try {
wc.pair(uri.trim())
uriText = ""
} catch (e: Exception) {
error = "Pairing failed: ${e.message}"
}
isPairing = false
}
}
if (showScanner) {
QrScannerScreen(
onQrCodeScanned = { scannedUri ->
showScanner = false
uriText = scannedUri
doPair(scannedUri)
},
onDismiss = { showScanner = false }
)
return
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("WalletConnect") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
)
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
// Scan QR button
Button(
onClick = { showScanner = true },
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme
.secondaryContainer,
contentColor = MaterialTheme.colorScheme
.onSecondaryContainer
)
) {
Icon(
Icons.Default.QrCodeScanner,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Scan QR Code")
}
Spacer(modifier = Modifier.height(16.dp))
// Divider
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
HorizontalDivider(modifier = Modifier.weight(1f))
Text(
" or paste URI ",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme
.onSurfaceVariant
)
HorizontalDivider(modifier = Modifier.weight(1f))
}
Spacer(modifier = Modifier.height(16.dp))
// URI input
OutlinedTextField(
value = uriText,
onValueChange = { uriText = it },
label = { Text("WalletConnect URI") },
placeholder = { Text("wc:...") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
if (uriText.isNotBlank()) doPair(uriText)
},
enabled = !isPairing,
modifier = Modifier.fillMaxWidth()
) {
if (isPairing) {
CircularProgressIndicator(
modifier = Modifier.size(18.dp),
strokeWidth = 2.dp,
color = MaterialTheme.colorScheme.onPrimary
)
} else {
Text("Pair")
}
}
if (error != null) {
Spacer(modifier = Modifier.height(8.dp))
Text(
error!!,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
}
Spacer(modifier = Modifier.height(24.dp))
Text(
"Active Sessions",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
if (sessions.isEmpty()) {
Box(
modifier = Modifier.fillMaxWidth().weight(1f),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
Icons.Default.LinkOff,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme
.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"No active sessions",
color = MaterialTheme.colorScheme
.onSurfaceVariant
)
Text(
"Scan a QR code or paste a URI",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme
.onSurfaceVariant
.copy(alpha = 0.7f)
)
}
}
} else {
Column(
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
) {
sessions.entries.forEach { (topic, session) ->
SessionCard(
session = session,
onDisconnect = {
scope.launch {
try {
wc.disconnectSession(topic)
} catch (e: Exception) {
error = "Disconnect failed: " +
"${e.message}"
}
}
}
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
}
}
@Composable
private fun SessionCard(
session: WcSession,
onDisconnect: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
) {
Column(modifier = Modifier.padding(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Column(modifier = Modifier.weight(1f)) {
Text(
session.peer.name,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.bodyLarge
)
if (session.peer.url.isNotEmpty()) {
Text(
session.peer.url,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme
.onSurfaceVariant
)
}
}
IconButton(onClick = onDisconnect) {
Icon(
Icons.Default.LinkOff,
contentDescription = "Disconnect",
tint = MaterialTheme.colorScheme.error
)
}
}
if (session.namespaces.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
session.namespaces.keys.forEach { ns ->
SuggestionChip(
onClick = {},
label = { Text(ns, fontSize = 11.sp) }
)
}
}
}
Text(
"Topic: ${session.topic.take(12)}...",
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme
.onSurfaceVariant.copy(alpha = 0.6f)
)
}
}
}
5. Build the QR Scanner
The QR scanner uses CameraX and ML Kit Barcode Scanning to detect WalletConnect QR codes.import android.Manifest
import android.util.Size
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
@androidx.annotation.OptIn(
androidx.camera.core.ExperimentalGetImage::class
)
@Composable
fun QrScannerScreen(
onQrCodeScanned: (String) -> Unit,
onDismiss: () -> Unit
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var hasCameraPermission by remember { mutableStateOf(false) }
var hasScanned by remember { mutableStateOf(false) }
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { granted ->
hasCameraPermission = granted
}
LaunchedEffect(Unit) {
permissionLauncher.launch(Manifest.permission.CAMERA)
}
Box(modifier = Modifier.fillMaxSize()) {
if (hasCameraPermission) {
AndroidView(
factory = { ctx ->
val previewView = PreviewView(ctx)
val cameraProviderFuture =
ProcessCameraProvider.getInstance(ctx)
cameraProviderFuture.addListener({
val cameraProvider =
cameraProviderFuture.get()
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(
previewView.surfaceProvider
)
}
val scanner =
BarcodeScanning.getClient()
val imageAnalysis = ImageAnalysis
.Builder()
.setTargetResolution(
Size(1280, 720)
)
.setBackpressureStrategy(
ImageAnalysis
.STRATEGY_KEEP_ONLY_LATEST
)
.build()
imageAnalysis.setAnalyzer(
ContextCompat.getMainExecutor(ctx)
) { imageProxy ->
val mediaImage = imageProxy.image
if (
mediaImage != null && !hasScanned
) {
val inputImage =
InputImage.fromMediaImage(
mediaImage,
imageProxy.imageInfo
.rotationDegrees
)
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
if (
barcode.format ==
Barcode.FORMAT_QR_CODE
) {
val value =
barcode.rawValue
if (
value != null
&& value.startsWith(
"wc:"
)
&& !hasScanned
) {
hasScanned = true
onQrCodeScanned(
value
)
}
}
}
}
.addOnCompleteListener {
imageProxy.close()
}
} else {
imageProxy.close()
}
}
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageAnalysis
)
}, ContextCompat.getMainExecutor(ctx))
previewView
},
modifier = Modifier.fillMaxSize()
)
// Scanning overlay
Canvas(modifier = Modifier.fillMaxSize()) {
val scanBoxSize = size.minDimension * 0.65f
val left = (size.width - scanBoxSize) / 2
val top = (size.height - scanBoxSize) / 2
drawRect(
color = Color.Black.copy(alpha = 0.5f)
)
drawRoundRect(
color = Color.Transparent,
topLeft = Offset(left, top),
size = androidx.compose.ui.geometry.Size(
scanBoxSize, scanBoxSize
),
cornerRadius = CornerRadius(16.dp.toPx()),
blendMode = BlendMode.Clear
)
drawRoundRect(
color = Color.White,
topLeft = Offset(left, top),
size = androidx.compose.ui.geometry.Size(
scanBoxSize, scanBoxSize
),
cornerRadius = CornerRadius(16.dp.toPx()),
style = Stroke(width = 2.dp.toPx())
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.padding(bottom = 100.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
"Scan WalletConnect QR Code",
color = Color.White,
style = MaterialTheme.typography.titleMedium
)
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("Camera permission is required to scan QR codes")
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
permissionLauncher.launch(
Manifest.permission.CAMERA
)
}) {
Text("Grant Permission")
}
}
}
// Close button
IconButton(
onClick = onDismiss,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(16.dp)
.statusBarsPadding(),
colors = IconButtonDefaults.iconButtonColors(
containerColor = Color.Black.copy(alpha = 0.5f),
contentColor = Color.White
)
) {
Icon(
Icons.Default.Close,
contentDescription = "Close scanner"
)
}
}
}
WalletConnect API Reference
WalletConnectModule
| Method | Description |
|---|---|
initialize() | Initialize WalletConnect. Call after user authentication. |
pair(uri: String) | Pair with a dApp using a WalletConnect URI from a QR code. |
confirmPairing(approved: Boolean) | Approve or reject a pending session proposal. |
respondSessionRequest(id: String, topic: String, approved: Boolean) | Approve or reject a signing request from a connected dApp. |
disconnectSession(topic: String) | Disconnect a specific session. |
Properties & Flows
| Property | Type | Description |
|---|---|---|
sessionsChanges | StateFlow<Map<String, WcSession>> | Reactive flow of active sessions. |
onSessionProposal | SharedFlow<WcSessionProposal> | Emits when a dApp proposes a new session. |
onSessionRequest | SharedFlow<WcSessionRequest> | Emits when a dApp sends a signing request. |
onSessionDelete | SharedFlow<String?> | Emits when a session is disconnected (topic). |
initializedChanges | StateFlow<Boolean> | Emits when WC initialization state changes. |
Data Models
data class WcSession(
val topic: String,
val peer: WcPeerMetadata,
val namespaces: Map<String, WcNamespace>,
val expiry: Int?
)
data class WcSessionProposal(
val id: Int,
val proposer: WcPeerMetadata,
val requiredNamespaces: Map<String, WcNamespace>?,
val optionalNamespaces: Map<String, WcNamespace>?
)
data class WcSessionRequest(
val id: String,
val topic: String,
val method: String, // e.g. "personal_sign", "eth_sendTransaction"
val chainId: String, // e.g. "eip155:1"
val params: JsonElement?
)
data class WcPeerMetadata(
val name: String,
val description: String,
val url: String,
val icons: List<String>
)
data class WcNamespace(
val chains: List<String>,
val methods: List<String>,
val events: List<String>,
val accounts: List<String>?
)
Best Practices
- Place
WcGlobalListenerat the app root so session proposals and signing requests are handled regardless of which screen the user is on. - Wait for both auth token and
initializedChangesbefore callingwc.initialize()— the Kotlin SDK requires both the user to be authenticated and the internal WebView to be ready. - Show clear approval UIs — display the dApp name, URL, and requested chains so users can make informed decisions.
- Handle disconnections gracefully — collect
onSessionDeleteand update your UI accordingly.