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:
parent
455f45ed59
commit
1dbc4c92c4
8 changed files with 646 additions and 4 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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?) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue