feat(wallet): add wallet setup flow and payment event wiring

Phase 4: Final features for Element X ADA alpha

## Wallet Setup Flow
- New setup state machine: WELCOME -> GENERATING -> ADDRESS -> BACKUP_PROMPT -> COMPLETE
- WalletSetupState.kt: state data class and events
- WalletSetupPresenter.kt: generates wallet via CardanoKeyStorage, state transitions
- WalletSetupView.kt: Compose UI with FLAG_SECURE for mnemonic display
- WalletSetupNode.kt: Appyx node with setup callbacks
- Wired into MessagesFlowNode via NavTarget.WalletSetup
- SSSS backup skipped for alpha (local-only, TODO for Phase 5)

## Payment Event Wiring
- PaymentProgressPresenter now sends Matrix payment event on tx confirmation
- Added roomId to PaymentProgressNode.Inputs and NavTarget.Progress
- Calls paymentEventSender.sendPaymentEvent() when SubmissionState.Confirmed
- Non-fatal if event fails (tx already succeeded)

## Files Changed
- features/wallet/impl/setup/ (new directory, 4 files)
- MessagesFlowNode.kt: NavTarget.WalletSetup, navigation wiring
- PaymentFlowNode.kt: roomId passthrough to Progress
- PaymentProgressNode.kt: roomId in Inputs
- PaymentProgressPresenter.kt: event sending on confirmation
This commit is contained in:
Kayos 2026-03-28 10:13:06 -07:00
parent 455f45ed59
commit 1dbc4c92c4
8 changed files with 646 additions and 4 deletions

View file

@ -54,6 +54,7 @@ import io.element.android.features.poll.api.create.CreatePollEntryPoint
import io.element.android.features.poll.api.create.CreatePollMode
import io.element.android.features.wallet.api.WalletEntryPoint
import io.element.android.features.wallet.impl.panel.WalletPanelNode
import io.element.android.features.wallet.impl.setup.WalletSetupNode
import io.element.android.libraries.architecture.BackstackWithOverlayBox
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.callback
@ -186,6 +187,9 @@ class MessagesFlowNode(
@Parcelize
data object WalletPanel : NavTarget
@Parcelize
data object WalletSetup : NavTarget
@Parcelize
data class PaymentFlow(
val roomId: RoomId,
@ -553,11 +557,24 @@ class MessagesFlowNode(
}
override fun onSetupWallet() {
// TODO: Navigate to wallet setup flow
backstack.push(NavTarget.WalletSetup)
}
}
createNode<WalletPanelNode>(buildContext, listOf(walletPanelCallback))
}
is NavTarget.WalletSetup -> {
val setupCallback = object : WalletSetupNode.Callback {
override fun onSetupComplete() {
// Pop setup, stay on wallet panel which will now show the wallet
backstack.pop()
}
override fun onBack() {
backstack.pop()
}
}
createNode<WalletSetupNode>(buildContext, listOf(setupCallback))
}
is NavTarget.PaymentFlow -> {
val walletCallback = object : WalletEntryPoint.Callback {
override fun onPaymentSent(txHash: String) {

View file

@ -87,6 +87,7 @@ class PaymentFlowNode(
data class Progress(
val recipientAddress: String,
val amountLovelace: Lovelace,
val roomId: RoomId,
) : NavTarget
}
@ -127,6 +128,7 @@ class PaymentFlowNode(
backstack.replace(NavTarget.Progress(
recipientAddress = navTarget.recipientAddress,
amountLovelace = navTarget.amountLovelace,
roomId = inputs.roomId,
))
}
@ -141,6 +143,7 @@ class PaymentFlowNode(
val nodeInputs = PaymentProgressNode.Inputs(
recipientAddress = navTarget.recipientAddress,
amountLovelace = navTarget.amountLovelace,
roomId = navTarget.roomId,
)
val nodeCallback = object : PaymentProgressNode.Callback {
override fun onPaymentComplete(txHash: String?) {

View file

@ -18,6 +18,7 @@ import io.element.android.annotations.ContributesNode
import io.element.android.features.wallet.impl.slash.Lovelace
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.parcelize.Parcelize
/**
@ -26,8 +27,7 @@ import kotlinx.parcelize.Parcelize
* Displays transaction submission progress and polls for confirmation.
*/
@ContributesNode(SessionScope::class)
@AssistedInject
class PaymentProgressNode(
class PaymentProgressNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenterFactory: PaymentProgressPresenter.Factory,
@ -37,6 +37,7 @@ class PaymentProgressNode(
data class Inputs(
val recipientAddress: String,
val amountLovelace: Lovelace,
val roomId: RoomId,
) : NodeInputs, Parcelable
interface Callback : Plugin {
@ -51,6 +52,7 @@ class PaymentProgressNode(
presenterFactory.create(
recipientAddress = inputs.recipientAddress,
amountLovelace = inputs.amountLovelace,
roomId = inputs.roomId,
)
}

View file

@ -16,8 +16,10 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.features.wallet.api.CardanoClient
import io.element.android.features.wallet.api.PaymentEventSender
import io.element.android.features.wallet.api.PaymentRequest
import io.element.android.features.wallet.api.PaymentStatusPoller
import io.element.android.features.wallet.api.SignedTransaction
import io.element.android.features.wallet.api.TransactionBuilder
import io.element.android.features.wallet.api.TxStatus
import io.element.android.features.wallet.impl.cardano.CardanoNetwork
@ -26,6 +28,8 @@ import io.element.android.features.wallet.impl.cardano.CardanoWalletManager
import io.element.android.features.wallet.impl.slash.Lovelace
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import timber.log.Timber
/**
@ -34,16 +38,18 @@ import timber.log.Timber
class PaymentProgressPresenter @AssistedInject constructor(
@Assisted private val recipientAddress: String,
@Assisted private val amountLovelace: Lovelace,
@Assisted private val roomId: RoomId,
private val matrixClient: MatrixClient,
private val walletManager: CardanoWalletManager,
private val transactionBuilder: TransactionBuilder,
private val cardanoClient: CardanoClient,
private val paymentStatusPoller: PaymentStatusPoller,
private val paymentEventSender: PaymentEventSender,
) : Presenter<PaymentProgressState> {
@AssistedFactory
interface Factory {
fun create(recipientAddress: String, amountLovelace: Lovelace): PaymentProgressPresenter
fun create(recipientAddress: String, amountLovelace: Lovelace, roomId: RoomId): PaymentProgressPresenter
}
companion object {
@ -61,6 +67,12 @@ class PaymentProgressPresenter @AssistedInject constructor(
var errorMessage by remember { mutableStateOf<String?>(null) }
var submissionStartTime by remember { mutableStateOf(0L) }
// Store for event sending
var lastRequest by remember { mutableStateOf<PaymentRequest?>(null) }
var lastSignedTx by remember { mutableStateOf<SignedTransaction?>(null) }
var eventSent by remember { mutableStateOf(false) }
// Build and submit
LaunchedEffect(Unit) {
submissionStartTime = System.currentTimeMillis()
submissionState = SubmissionState.Submitting
@ -84,6 +96,8 @@ class PaymentProgressPresenter @AssistedInject constructor(
transactionBuilder.buildAndSign(request).onSuccess { signedTx ->
Timber.tag(TAG).d("Transaction built successfully, hash: ${signedTx.txHash}")
txHash = signedTx.txHash
lastRequest = request
lastSignedTx = signedTx
cardanoClient.submitTx(signedTx.txCbor).onSuccess { submittedHash ->
Timber.tag(TAG).i("Transaction submitted: $submittedHash")
@ -100,6 +114,7 @@ class PaymentProgressPresenter @AssistedInject constructor(
}
}
// Poll for confirmation
val currentTxHash = txHash
LaunchedEffect(currentTxHash) {
if (currentTxHash == null) return@LaunchedEffect
@ -130,6 +145,36 @@ class PaymentProgressPresenter @AssistedInject constructor(
}
}
// Send Matrix event on confirmation
LaunchedEffect(submissionState, eventSent) {
if (submissionState == SubmissionState.Confirmed && !eventSent) {
val req = lastRequest ?: return@LaunchedEffect
val signedTx = lastSignedTx ?: return@LaunchedEffect
eventSent = true
val room = matrixClient.getRoom(roomId)
val joinedRoom = room as? JoinedRoom
val timeline = joinedRoom?.liveTimeline
if (timeline != null) {
paymentEventSender.sendPaymentEvent(
timeline = timeline,
request = req,
signedTx = signedTx,
network = CardanoNetworkConfig.NETWORK_NAME,
).onSuccess {
Timber.tag(TAG).i("Payment event sent to timeline")
}.onFailure { e ->
Timber.tag(TAG).e(e, "Failed to send payment event to timeline")
// Non-fatal - tx succeeded, just event didn't send
}
} else {
Timber.tag(TAG).w("Could not get room timeline to send payment event")
}
}
}
val explorerUrl = txHash?.let {
"${CardanoNetworkConfig.EXPLORER_BASE_URL}/transaction/$it"
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2026 Sulkta Coop.
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
package io.element.android.features.wallet.impl.setup
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
class WalletSetupNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: WalletSetupPresenter,
) : Node(buildContext = buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onSetupComplete()
fun onBack()
}
private val callback: Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
WalletSetupView(
state = state,
onComplete = { callback.onSetupComplete() },
onBack = { callback.onBack() },
modifier = modifier,
)
}
}

View file

@ -0,0 +1,124 @@
/*
* Copyright (c) 2026 Sulkta Coop.
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
package io.element.android.features.wallet.impl.setup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.features.wallet.api.storage.CardanoKeyStorage
import io.element.android.features.wallet.impl.cardano.CardanoWalletManager
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.launch
import timber.log.Timber
// TODO: Phase 5 - Add optional SSSS backup
// When Matrix SDK exposes setAccountData, store encrypted mnemonic
// under m.cross_signing.user_signing_key or custom type.
// For alpha: wallet backup is LOCAL ONLY (device-bound).
// User must write down mnemonic manually.
class WalletSetupPresenter @Inject constructor(
private val keyStorage: CardanoKeyStorage,
private val walletManager: CardanoWalletManager,
private val matrixClient: MatrixClient,
) : Presenter<WalletSetupState> {
companion object {
private const val TAG = "WalletSetupPresenter"
}
@Composable
override fun present(): WalletSetupState {
val scope = rememberCoroutineScope()
val sessionId = matrixClient.sessionId
var step by remember { mutableStateOf(SetupStep.WELCOME) }
var generatedMnemonic by remember { mutableStateOf<List<String>>(emptyList()) }
var generatedAddress by remember { mutableStateOf<String?>(null) }
var isGenerating by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<String?>(null) }
var hasConfirmedBackup by remember { mutableStateOf(false) }
fun handleEvent(event: WalletSetupEvent) {
when (event) {
WalletSetupEvent.CreateNewWallet -> {
step = SetupStep.GENERATING
isGenerating = true
error = null
scope.launch {
keyStorage.generateWallet(sessionId)
.onSuccess { result ->
Timber.tag(TAG).i("Wallet generated: ${result.baseAddress.take(20)}...")
generatedMnemonic = result.mnemonic
generatedAddress = result.baseAddress
isGenerating = false
step = SetupStep.SHOW_ADDRESS
}
.onFailure { e ->
Timber.tag(TAG).e(e, "Failed to generate wallet")
error = e.message ?: "Failed to generate wallet"
isGenerating = false
step = SetupStep.WELCOME
}
}
}
WalletSetupEvent.ImportExistingWallet -> {
// TODO: Navigate to import flow (out of scope for alpha)
// For now, just show an error
error = "Import not yet supported. Please create a new wallet."
}
WalletSetupEvent.ProceedToBackup -> {
step = SetupStep.BACKUP_PROMPT
}
WalletSetupEvent.ConfirmBackup -> {
hasConfirmedBackup = true
step = SetupStep.COMPLETE
// Reinitialize wallet manager so panel sees the new wallet
scope.launch {
walletManager.initialize(sessionId)
}
}
WalletSetupEvent.Complete -> {
// Callback handled by node
}
WalletSetupEvent.Back -> {
when (step) {
SetupStep.SHOW_ADDRESS -> step = SetupStep.WELCOME
SetupStep.BACKUP_PROMPT -> step = SetupStep.SHOW_ADDRESS
else -> { /* Let node handle close */ }
}
}
WalletSetupEvent.DismissError -> {
error = null
}
}
}
return WalletSetupState(
step = step,
generatedMnemonic = generatedMnemonic,
generatedAddress = generatedAddress,
isGenerating = isGenerating,
error = error,
hasConfirmedBackup = hasConfirmedBackup,
eventSink = ::handleEvent,
)
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2026 Sulkta Coop.
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
package io.element.android.features.wallet.impl.setup
import androidx.compose.runtime.Immutable
@Immutable
data class WalletSetupState(
val step: SetupStep,
val generatedMnemonic: List<String>,
val generatedAddress: String?,
val isGenerating: Boolean,
val error: String?,
val hasConfirmedBackup: Boolean,
val eventSink: (WalletSetupEvent) -> Unit,
)
enum class SetupStep {
WELCOME, // "Create New Wallet" or "Import Existing"
GENERATING, // Spinning while generating keys
SHOW_ADDRESS, // Display the derived address
BACKUP_PROMPT, // Show mnemonic with "I've backed it up" checkbox
COMPLETE, // Done - ready to close
}
sealed interface WalletSetupEvent {
data object CreateNewWallet : WalletSetupEvent
data object ImportExistingWallet : WalletSetupEvent
data object ProceedToBackup : WalletSetupEvent
data object ConfirmBackup : WalletSetupEvent
data object Complete : WalletSetupEvent
data object Back : WalletSetupEvent
data object DismissError : WalletSetupEvent
}

View file

@ -0,0 +1,368 @@
/*
* Copyright (c) 2026 Sulkta Coop.
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
package io.element.android.features.wallet.impl.setup
import android.view.WindowManager
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Download
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WalletSetupView(
state: WalletSetupState,
onComplete: () -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
// FLAG_SECURE when showing mnemonic
val view = LocalView.current
DisposableEffect(state.step) {
if (state.step == SetupStep.BACKUP_PROMPT) {
val window = (view.context as? android.app.Activity)?.window
window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
onDispose { window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) }
} else {
onDispose { }
}
}
Scaffold(
modifier = modifier.fillMaxSize().systemBarsPadding(),
topBar = {
TopAppBar(
title = { Text("Set Up Wallet") },
navigationIcon = {
if (state.step != SetupStep.COMPLETE) {
IconButton(onClick = {
if (state.step == SetupStep.WELCOME) {
onBack()
} else {
state.eventSink(WalletSetupEvent.Back)
}
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
}
}
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
when (state.step) {
SetupStep.WELCOME -> WelcomeContent(state)
SetupStep.GENERATING -> GeneratingContent()
SetupStep.SHOW_ADDRESS -> AddressContent(state)
SetupStep.BACKUP_PROMPT -> BackupContent(state)
SetupStep.COMPLETE -> CompleteContent(onComplete)
}
}
}
}
@Composable
private fun ColumnScope.WelcomeContent(state: WalletSetupState) {
Spacer(modifier = Modifier.height(48.dp))
Text(
text = "Create Your Wallet",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Your Cardano wallet will be secured with your device's biometric authentication.",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.weight(1f))
Button(
text = "Create New Wallet",
onClick = { state.eventSink(WalletSetupEvent.CreateNewWallet) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = IconSource.Vector(Icons.Default.Add),
)
Spacer(modifier = Modifier.height(12.dp))
OutlinedButton(
text = "Import Existing Wallet",
onClick = { state.eventSink(WalletSetupEvent.ImportExistingWallet) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = IconSource.Vector(Icons.Default.Download),
)
Spacer(modifier = Modifier.height(32.dp))
state.error?.let { error ->
Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer),
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = error,
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.onErrorContainer,
)
}
Spacer(modifier = Modifier.height(16.dp))
}
}
@Composable
private fun ColumnScope.GeneratingContent() {
Spacer(modifier = Modifier.weight(1f))
CircularProgressIndicator(modifier = Modifier.size(64.dp))
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Generating Wallet...",
style = MaterialTheme.typography.titleLarge,
)
Text(
text = "Creating secure keys",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.weight(1f))
}
@Composable
private fun ColumnScope.AddressContent(state: WalletSetupState) {
Spacer(modifier = Modifier.height(48.dp))
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Wallet Created!",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Your Cardano Address:",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.height(8.dp))
Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = state.generatedAddress ?: "",
modifier = Modifier.padding(16.dp),
fontFamily = FontFamily.Monospace,
style = MaterialTheme.typography.bodySmall,
)
}
Spacer(modifier = Modifier.weight(1f))
Button(
text = "Continue to Backup",
onClick = { state.eventSink(WalletSetupEvent.ProceedToBackup) },
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(32.dp))
}
@Composable
private fun ColumnScope.BackupContent(state: WalletSetupState) {
var isChecked by remember { mutableStateOf(false) }
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Back Up Your Wallet",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
)
Spacer(modifier = Modifier.height(8.dp))
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.5f)
),
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = "⚠️ Write down these 24 words in order. Anyone with this phrase can access your funds.",
modifier = Modifier.padding(12.dp),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onErrorContainer,
)
}
Spacer(modifier = Modifier.height(16.dp))
LazyVerticalGrid(
columns = GridCells.Fixed(3),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.weight(1f),
) {
itemsIndexed(state.generatedMnemonic) { index, word ->
Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "${index + 1}.",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = word,
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
modifier = Modifier.padding(start = 4.dp),
)
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Checkbox(
checked = isChecked,
onCheckedChange = { isChecked = it },
)
Text(
text = "I have written down my recovery phrase",
style = MaterialTheme.typography.bodyMedium,
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(
text = "Complete Setup",
onClick = { state.eventSink(WalletSetupEvent.ConfirmBackup) },
enabled = isChecked,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(32.dp))
}
@Composable
private fun ColumnScope.CompleteContent(onComplete: () -> Unit) {
Spacer(modifier = Modifier.weight(1f))
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
modifier = Modifier.size(80.dp),
tint = MaterialTheme.colorScheme.primary,
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "You're All Set!",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Your wallet is ready to use.",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.weight(1f))
Button(
text = "Done",
onClick = onComplete,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(32.dp))
}