mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2025-02-25 08:51:20 -05:00
demo: initial send functionality on wallet screen
This commit is contained in:
parent
7f167fb145
commit
abb43563e3
@ -63,6 +63,10 @@ class WalletRepository(
|
||||
fun getTransaction(walletId: Long, txId: String): Flow<Transaction?> =
|
||||
getLedger(walletId).map { it.transactionById[txId] }
|
||||
|
||||
suspend fun createTransfer(walletId: Long, transferRequest: TransferRequest): PendingTransfer {
|
||||
return getWallet(walletId).createTransfer(transferRequest)
|
||||
}
|
||||
|
||||
suspend fun addWallet(
|
||||
moneroNetwork: MoneroNetwork,
|
||||
name: String,
|
||||
|
@ -170,10 +170,10 @@ private fun SecondStepScreen(
|
||||
)
|
||||
SelectListBox(
|
||||
label = "Network",
|
||||
options = MoneroNetwork.values().map { it.name },
|
||||
selectedOption = network.name,
|
||||
options = MoneroNetwork.entries.associateWith { it.name },
|
||||
selectedOption = network,
|
||||
onOptionClick = {
|
||||
onNetworkChanged(MoneroNetwork.valueOf(it))
|
||||
onNetworkChanged(it)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
@ -7,14 +7,11 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.molly.monero.demo.data.model.WalletAddress
|
||||
import im.molly.monero.demo.ui.component.CopyableText
|
||||
import im.molly.monero.demo.ui.theme.Blue40
|
||||
import im.molly.monero.demo.ui.theme.Red40
|
||||
|
||||
@Composable
|
||||
fun AddressCardExpanded(
|
||||
@ -47,18 +44,14 @@ fun AddressCardExpanded(
|
||||
modifier = if (used) Modifier.alpha(0.5f) else Modifier,
|
||||
)
|
||||
if (walletAddress.isLastForAccount) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
TextButton(onClick = onCreateSubAddressClick) {
|
||||
Text(
|
||||
text = "Add subaddress",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
TextButton(onClick = onCreateSubAddressClick) {
|
||||
Text(
|
||||
text = "Add subaddress",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,10 +67,10 @@ private fun EditRemoteNodeDialog(
|
||||
)
|
||||
SelectListBox(
|
||||
label = "Network",
|
||||
options = MoneroNetwork.values().map { it.name },
|
||||
selectedOption = remoteNode.network.name,
|
||||
options = MoneroNetwork.entries.associateWith { it.name },
|
||||
selectedOption = remoteNode.network,
|
||||
onOptionClick = {
|
||||
onRemoteNodeChange(remoteNode.copy(network = MoneroNetwork.valueOf(it)))
|
||||
onRemoteNodeChange(remoteNode.copy(network = it))
|
||||
}
|
||||
)
|
||||
Column {
|
||||
|
@ -0,0 +1,123 @@
|
||||
package im.molly.monero.demo.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.molly.monero.MoneroCurrency
|
||||
|
||||
@Composable
|
||||
fun EditableRecipientList(
|
||||
recipients: List<Pair<String, String>>,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
onRecipientChange: (List<Pair<String, String>>) -> Unit = {},
|
||||
) {
|
||||
val updatedList = recipients.toMutableList()
|
||||
|
||||
if (updatedList.isEmpty()) {
|
||||
updatedList.add("" to "")
|
||||
onRecipientChange(updatedList)
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
updatedList.forEachIndexed { index, (address, amount) ->
|
||||
PaymentDetailItem(
|
||||
itemIndex = index,
|
||||
amount = amount,
|
||||
address = address,
|
||||
enabled = enabled,
|
||||
onAmountChange = {
|
||||
updatedList[index] = address to it
|
||||
onRecipientChange(updatedList)
|
||||
},
|
||||
onAddressChange = {
|
||||
updatedList[index] = it to amount
|
||||
onRecipientChange(updatedList)
|
||||
},
|
||||
onDeleteItemClick = {
|
||||
updatedList.removeAt(index)
|
||||
onRecipientChange(updatedList)
|
||||
}
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
onClick = { onRecipientChange(updatedList + ("" to "")) },
|
||||
enabled = enabled,
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
) {
|
||||
Text(text = "Add recipient")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PaymentDetailItem(
|
||||
itemIndex: Int,
|
||||
address: String,
|
||||
amount: String,
|
||||
enabled: Boolean = true,
|
||||
onAmountChange: (String) -> Unit,
|
||||
onAddressChange: (String) -> Unit,
|
||||
onDeleteItemClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text("Recipient") },
|
||||
singleLine = true,
|
||||
value = address,
|
||||
isError = address.isBlank(),
|
||||
enabled = enabled,
|
||||
onValueChange = onAddressChange,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
OutlinedTextField(
|
||||
label = { Text("Amount") },
|
||||
placeholder = { Text("0.00") },
|
||||
singleLine = true,
|
||||
value = amount,
|
||||
suffix = { Text(MoneroCurrency.SYMBOL) },
|
||||
isError = amount.isBlank(),
|
||||
enabled = enabled,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
onValueChange = onAmountChange,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 8.dp),
|
||||
)
|
||||
IconButton(
|
||||
onClick = onDeleteItemClick,
|
||||
enabled = enabled && itemIndex > 0,
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.Delete, contentDescription = "Delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun EditableRecipientListPreview() {
|
||||
EditableRecipientList(
|
||||
recipients = listOf(
|
||||
"888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H" to "0.01"
|
||||
),
|
||||
)
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
package im.molly.monero.demo.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import im.molly.monero.FeePriority
|
||||
import im.molly.monero.MoneroCurrency
|
||||
import im.molly.monero.PaymentDetail
|
||||
import im.molly.monero.PaymentRequest
|
||||
import im.molly.monero.PendingTransfer
|
||||
import im.molly.monero.PublicAddress
|
||||
import im.molly.monero.TransferRequest
|
||||
import im.molly.monero.demo.AppModule
|
||||
import im.molly.monero.demo.data.WalletRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SendTabViewModel(
|
||||
private val walletId: Long,
|
||||
private val walletRepository: WalletRepository = AppModule.walletRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
private val viewModelState = MutableStateFlow(SendTabUiState())
|
||||
|
||||
val uiState = viewModelState.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = viewModelState.value
|
||||
)
|
||||
|
||||
fun updateAccount(accountIndex: Int) {
|
||||
viewModelState.update {
|
||||
it.copy(
|
||||
accountIndex = accountIndex,
|
||||
status = TransferStatus.Idle,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePriority(priority: FeePriority) {
|
||||
viewModelState.update {
|
||||
it.copy(
|
||||
feePriority = priority,
|
||||
status = TransferStatus.Idle,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateRecipients(recipients: List<Pair<String, String>>) {
|
||||
viewModelState.update {
|
||||
it.copy(
|
||||
recipients = recipients, status = TransferStatus.Idle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPaymentRequest(state: SendTabUiState): Result<PaymentRequest> {
|
||||
return runCatching {
|
||||
PaymentRequest(
|
||||
spendingAccountIndex = state.accountIndex,
|
||||
paymentDetails = state.recipients.map { (address, amount) ->
|
||||
PaymentDetail(
|
||||
recipientAddress = PublicAddress.parse(address),
|
||||
amount = MoneroCurrency.parse(amount),
|
||||
)
|
||||
},
|
||||
feePriority = state.feePriority,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createPayment() {
|
||||
val result = getPaymentRequest(viewModelState.value)
|
||||
result.fold(
|
||||
onSuccess = { createTransfer(it) },
|
||||
onFailure = { error ->
|
||||
viewModelState.update {
|
||||
it.copy(status = TransferStatus.Error(error.message))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTransfer(transferRequest: TransferRequest) {
|
||||
viewModelState.update {
|
||||
it.copy(status = TransferStatus.Preparing)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val pendingTransfer = runCatching {
|
||||
walletRepository.createTransfer(walletId, transferRequest)
|
||||
}
|
||||
viewModelState.update {
|
||||
val updatedState = pendingTransfer.fold(
|
||||
onSuccess = { pendingTransfer ->
|
||||
it.copy(status = TransferStatus.ReadyForApproval(pendingTransfer))
|
||||
},
|
||||
onFailure = { error ->
|
||||
it.copy(status = TransferStatus.Error(error.message))
|
||||
},
|
||||
)
|
||||
updatedState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun confirmPayment() {
|
||||
TODO()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun factory(walletId: Long) = viewModelFactory {
|
||||
initializer {
|
||||
SendTabViewModel(walletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class SendTabUiState(
|
||||
val accountIndex: Int = 0,
|
||||
val recipients: List<Pair<String, String>> = emptyList(),
|
||||
val feePriority: FeePriority = FeePriority.Medium,
|
||||
val status: TransferStatus = TransferStatus.Idle,
|
||||
) {
|
||||
val isInProgress: Boolean
|
||||
get() = !(status == TransferStatus.Idle || status is TransferStatus.Error)
|
||||
}
|
||||
|
||||
sealed interface TransferStatus {
|
||||
data object Idle : TransferStatus
|
||||
data object Preparing : TransferStatus
|
||||
data class ReadyForApproval(
|
||||
val pendingTransfer: PendingTransfer
|
||||
) : TransferStatus
|
||||
|
||||
data class Error(val errorMessage: String?) : TransferStatus
|
||||
}
|
@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -55,17 +55,17 @@ fun WalletBalanceView(
|
||||
) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
text = "Balance at ${blockchainTime}",
|
||||
text = "Balance at $blockchainTime",
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
BalanceRow("Confirmed", balance.confirmedAmount)
|
||||
BalanceRow("Pending", balance.pendingAmount)
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
BalanceRow("Total", balance.totalAmount)
|
||||
|
||||
val currentTime = blockchainTime.copy(timestamp = now)
|
||||
val currentTime = blockchainTime.withTimestamp(now)
|
||||
|
||||
BalanceRow("Unlocked", balance.unlockedAmountAt(currentTime))
|
||||
balance.lockedAmountsAt(currentTime).forEach { (timeSpan, amount) ->
|
||||
|
@ -3,6 +3,10 @@ package im.molly.monero.demo.ui
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
@ -19,14 +23,19 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import im.molly.monero.FeePriority
|
||||
import im.molly.monero.Ledger
|
||||
import im.molly.monero.MoneroCurrency
|
||||
import im.molly.monero.demo.data.model.WalletConfig
|
||||
import im.molly.monero.demo.ui.component.CopyableText
|
||||
import im.molly.monero.demo.ui.component.SelectListBox
|
||||
import im.molly.monero.demo.ui.component.Toolbar
|
||||
import im.molly.monero.demo.ui.preview.PreviewParameterData
|
||||
import im.molly.monero.demo.ui.theme.AppIcons
|
||||
import im.molly.monero.demo.ui.theme.AppTheme
|
||||
import im.molly.monero.toFormattedString
|
||||
import kotlinx.coroutines.delay
|
||||
import java.time.Instant
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun WalletRoute(
|
||||
@ -34,20 +43,32 @@ fun WalletRoute(
|
||||
onTransactionClick: (String, Long) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: WalletViewModel = viewModel(
|
||||
walletViewModel: WalletViewModel = viewModel(
|
||||
factory = WalletViewModel.factory(walletId),
|
||||
key = WalletViewModel.key(walletId),
|
||||
)
|
||||
),
|
||||
sendTabViewModel: SendTabViewModel = viewModel(
|
||||
factory = SendTabViewModel.factory(walletId)
|
||||
),
|
||||
) {
|
||||
val uiState: WalletUiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val walletUiState by walletViewModel.uiState.collectAsStateWithLifecycle()
|
||||
val sendTabUiState by sendTabViewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
WalletScreen(
|
||||
uiState = uiState,
|
||||
onWalletConfigChange = { config -> viewModel.updateConfig(config) },
|
||||
onTransactionClick = onTransactionClick,
|
||||
onCreateAccountClick = { viewModel.createAccount() },
|
||||
onCreateSubAddressClick = { accountIndex ->
|
||||
viewModel.createSubAddress(accountIndex)
|
||||
walletUiState = walletUiState,
|
||||
sendTabUiState = sendTabUiState,
|
||||
onWalletConfigChange = { config ->
|
||||
walletViewModel.updateConfig(config)
|
||||
},
|
||||
onTransactionClick = onTransactionClick,
|
||||
onCreateAccountClick = { walletViewModel.createAccount() },
|
||||
onCreateSubAddressClick = { accountIndex ->
|
||||
walletViewModel.createSubAddress(accountIndex)
|
||||
},
|
||||
onTransferAccountSelect = { sendTabViewModel.updateAccount(it) },
|
||||
onTransferPrioritySelect = { sendTabViewModel.updatePriority(it) },
|
||||
onTransferRecipientChange = { sendTabViewModel.updateRecipients(it) },
|
||||
onTransferSendClick = { sendTabViewModel.createPayment() },
|
||||
onBackClick = onBackClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
@ -55,21 +76,31 @@ fun WalletRoute(
|
||||
|
||||
@Composable
|
||||
private fun WalletScreen(
|
||||
uiState: WalletUiState,
|
||||
onWalletConfigChange: (WalletConfig) -> Unit,
|
||||
onTransactionClick: (String, Long) -> Unit,
|
||||
onCreateAccountClick: () -> Unit,
|
||||
onCreateSubAddressClick: (Int) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
walletUiState: WalletUiState,
|
||||
sendTabUiState: SendTabUiState,
|
||||
modifier: Modifier = Modifier,
|
||||
onWalletConfigChange: (WalletConfig) -> Unit = {},
|
||||
onTransactionClick: (String, Long) -> Unit = { _, _ -> },
|
||||
onCreateAccountClick: () -> Unit = {},
|
||||
onCreateSubAddressClick: (Int) -> Unit = {},
|
||||
onTransferAccountSelect: (Int) -> Unit = {},
|
||||
onTransferPrioritySelect: (FeePriority) -> Unit = {},
|
||||
onTransferRecipientChange: (List<Pair<String, String>>) -> Unit = {},
|
||||
onTransferSendClick: () -> Unit = {},
|
||||
onBackClick: () -> Unit = {},
|
||||
) {
|
||||
when (uiState) {
|
||||
when (walletUiState) {
|
||||
is WalletUiState.Loaded -> WalletScreenLoaded(
|
||||
uiState = uiState,
|
||||
walletUiState = walletUiState,
|
||||
sendTabUiState = sendTabUiState,
|
||||
onWalletConfigChange = onWalletConfigChange,
|
||||
onTransactionClick = onTransactionClick,
|
||||
onCreateAccountClick = onCreateAccountClick,
|
||||
onCreateSubAddressClick = onCreateSubAddressClick,
|
||||
onTransferAccountSelect = onTransferAccountSelect,
|
||||
onTransferPrioritySelect = onTransferPrioritySelect,
|
||||
onTransferRecipientChange = onTransferRecipientChange,
|
||||
onTransferSendClick = onTransferSendClick,
|
||||
onBackClick = onBackClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
@ -80,13 +111,33 @@ private fun WalletScreen(
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun WalletToolbar(
|
||||
onBackClick: () -> Unit,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
Toolbar(navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
)
|
||||
}
|
||||
}, actions = actions)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WalletScreenLoaded(
|
||||
uiState: WalletUiState.Loaded,
|
||||
walletUiState: WalletUiState.Loaded,
|
||||
sendTabUiState: SendTabUiState,
|
||||
onWalletConfigChange: (WalletConfig) -> Unit,
|
||||
onTransactionClick: (String, Long) -> Unit,
|
||||
onCreateAccountClick: () -> Unit,
|
||||
onCreateSubAddressClick: (Int) -> Unit,
|
||||
onTransferAccountSelect: (Int) -> Unit,
|
||||
onTransferPrioritySelect: (FeePriority) -> Unit,
|
||||
onTransferRecipientChange: (List<Pair<String, String>>) -> Unit,
|
||||
onTransferSendClick: () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@ -96,14 +147,7 @@ private fun WalletScreenLoaded(
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Toolbar(navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
)
|
||||
}
|
||||
}, actions = {
|
||||
WalletToolbar(onBackClick = onBackClick, actions = {
|
||||
WalletKebabMenu(
|
||||
onRenameClick = { showRenameDialog = true },
|
||||
onDeleteClick = { },
|
||||
@ -122,11 +166,11 @@ private fun WalletScreenLoaded(
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(
|
||||
MoneroCurrency.Format(precision = 5)
|
||||
.format(uiState.balance.confirmedAmount)
|
||||
.format(walletUiState.totalBalance.confirmedAmount)
|
||||
)
|
||||
}
|
||||
})
|
||||
Text(text = uiState.config.name, style = MaterialTheme.typography.headlineSmall)
|
||||
Text(text = walletUiState.config.name, style = MaterialTheme.typography.headlineSmall)
|
||||
|
||||
WalletHeaderTabs(
|
||||
titles = listOf("Balance", "Send", "Receive", "History"),
|
||||
@ -137,12 +181,20 @@ private fun WalletScreenLoaded(
|
||||
when (selectedTabIndex) {
|
||||
0 -> {
|
||||
WalletBalanceView(
|
||||
balance = uiState.balance,
|
||||
blockchainTime = uiState.blockchainTime
|
||||
balance = walletUiState.totalBalance,
|
||||
blockchainTime = walletUiState.blockchainTime
|
||||
)
|
||||
}
|
||||
|
||||
1 -> {} // TODO
|
||||
1 -> {
|
||||
WalletSendTab(
|
||||
walletUiState, sendTabUiState,
|
||||
onAccountSelect = onTransferAccountSelect,
|
||||
onPrioritySelect = onTransferPrioritySelect,
|
||||
onRecipientChange = onTransferRecipientChange,
|
||||
onSendClick = onTransferSendClick,
|
||||
)
|
||||
}
|
||||
|
||||
2 -> {
|
||||
val scrollState = rememberLazyListState()
|
||||
@ -151,23 +203,18 @@ private fun WalletScreenLoaded(
|
||||
state = scrollState,
|
||||
) {
|
||||
addressCardItems(
|
||||
items = uiState.addresses,
|
||||
items = walletUiState.addresses,
|
||||
onCreateSubAddressClick = onCreateSubAddressClick,
|
||||
)
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
TextButton(
|
||||
onClick = onCreateAccountClick,
|
||||
modifier = modifier.padding(start = 16.dp, bottom = 8.dp),
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onCreateAccountClick,
|
||||
modifier = modifier.padding(bottom = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Create new account",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "Create new account",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -177,10 +224,10 @@ private fun WalletScreenLoaded(
|
||||
val scrollState = rememberLazyListState()
|
||||
|
||||
var lastTxId by remember { mutableStateOf("") }
|
||||
lastTxId = uiState.transactions.firstOrNull()?.transaction?.txId ?: ""
|
||||
lastTxId = walletUiState.transactions.firstOrNull()?.transaction?.txId ?: ""
|
||||
|
||||
LaunchedEffect(lastTxId) {
|
||||
if (uiState.transactions.isNotEmpty()) {
|
||||
if (walletUiState.transactions.isNotEmpty()) {
|
||||
scrollState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
@ -189,7 +236,7 @@ private fun WalletScreenLoaded(
|
||||
state = scrollState,
|
||||
) {
|
||||
transactionCardItems(
|
||||
items = uiState.transactions,
|
||||
items = walletUiState.transactions,
|
||||
onTransactionClick = onTransactionClick,
|
||||
)
|
||||
}
|
||||
@ -198,7 +245,7 @@ private fun WalletScreenLoaded(
|
||||
}
|
||||
|
||||
if (showRenameDialog) {
|
||||
var name by remember { mutableStateOf(uiState.config.name) }
|
||||
var name by remember { mutableStateOf(walletUiState.config.name) }
|
||||
AlertDialog(
|
||||
onDismissRequest = { showRenameDialog = false },
|
||||
title = { Text("Enter wallet name") },
|
||||
@ -211,7 +258,7 @@ private fun WalletScreenLoaded(
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onWalletConfigChange(uiState.config.copy(name = name))
|
||||
onWalletConfigChange(walletUiState.config.copy(name = name))
|
||||
showRenameDialog = false
|
||||
}) {
|
||||
Text("Rename")
|
||||
@ -222,16 +269,100 @@ private fun WalletScreenLoaded(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WalletSendTab(
|
||||
walletUiState: WalletUiState.Loaded,
|
||||
sendTabUiState: SendTabUiState,
|
||||
onAccountSelect: (Int) -> Unit,
|
||||
onPrioritySelect: (FeePriority) -> Unit,
|
||||
onRecipientChange: (List<Pair<String, String>>) -> Unit,
|
||||
onSendClick: () -> Unit,
|
||||
) {
|
||||
var now by remember { mutableStateOf(Instant.now()) }
|
||||
|
||||
LaunchedEffect(now) {
|
||||
delay(2.seconds)
|
||||
now = Instant.now()
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.imePadding()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
SelectListBox(
|
||||
label = "Send from",
|
||||
options = walletUiState.accountBalance.mapValues { (index, balance) ->
|
||||
val currentTime = walletUiState.blockchainTime.withTimestamp(now)
|
||||
val funds = balance.unlockedAmountAt(currentTime)
|
||||
val fundsFormatted = funds.toFormattedString(appendSymbol = true)
|
||||
"Account #$index : $fundsFormatted"
|
||||
},
|
||||
selectedOption = sendTabUiState.accountIndex,
|
||||
onOptionClick = { option -> onAccountSelect(option) },
|
||||
enabled = !sendTabUiState.isInProgress,
|
||||
)
|
||||
EditableRecipientList(
|
||||
recipients = sendTabUiState.recipients,
|
||||
onRecipientChange = onRecipientChange,
|
||||
enabled = !sendTabUiState.isInProgress,
|
||||
)
|
||||
SelectListBox(
|
||||
label = "Transaction priority",
|
||||
options = FeePriority.entries.associateWith { it.name },
|
||||
selectedOption = sendTabUiState.feePriority,
|
||||
onOptionClick = { option -> onPrioritySelect(option) },
|
||||
enabled = !sendTabUiState.isInProgress,
|
||||
)
|
||||
|
||||
if (sendTabUiState.status is TransferStatus.Error) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(top = 24.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Info,
|
||||
contentDescription = "Error Icon",
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
)
|
||||
Text(
|
||||
text = sendTabUiState.status.errorMessage
|
||||
?: "Unspecific error",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ElevatedButton(
|
||||
modifier = Modifier.padding(vertical = 24.dp),
|
||||
onClick = onSendClick,
|
||||
enabled = !sendTabUiState.isInProgress,
|
||||
) {
|
||||
val text = when (sendTabUiState.status) {
|
||||
TransferStatus.Preparing -> "Preparing..."
|
||||
else -> "Send"
|
||||
}
|
||||
Text(text = text)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WalletScreenError(
|
||||
onBackClick: () -> Unit,
|
||||
) {
|
||||
Scaffold(topBar = { WalletToolbar(onBackClick = onBackClick) }) {}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WalletScreenLoading(
|
||||
onBackClick: () -> Unit,
|
||||
) {
|
||||
Scaffold(topBar = { WalletToolbar(onBackClick = onBackClick) }) {}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -300,7 +431,7 @@ private fun WalletScreenPopulated(
|
||||
) {
|
||||
AppTheme {
|
||||
WalletScreen(
|
||||
uiState = WalletUiState.Loaded(
|
||||
walletUiState = WalletUiState.Loaded(
|
||||
config = WalletConfig(
|
||||
id = 0,
|
||||
publicAddress = ledger.publicAddress.address,
|
||||
@ -309,21 +440,21 @@ private fun WalletScreenPopulated(
|
||||
remoteNodes = emptySet(),
|
||||
),
|
||||
network = ledger.publicAddress.network,
|
||||
balance = ledger.balance,
|
||||
totalBalance = ledger.getBalance(),
|
||||
accountBalance = emptyMap(),
|
||||
blockchainTime = ledger.checkedAt,
|
||||
addresses = emptyList(),
|
||||
transactions = emptyList(),
|
||||
),
|
||||
onWalletConfigChange = {},
|
||||
onTransactionClick = { _: String, _: Long -> },
|
||||
onCreateAccountClick = {},
|
||||
onCreateSubAddressClick = {},
|
||||
onBackClick = {},
|
||||
sendTabUiState = SendTabUiState(
|
||||
accountIndex = 0,
|
||||
recipients = emptyList(),
|
||||
feePriority = FeePriority.Medium,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class WalletScreenPreviewParameterProvider :
|
||||
PreviewParameterProvider<Ledger> {
|
||||
private class WalletScreenPreviewParameterProvider : PreviewParameterProvider<Ledger> {
|
||||
override val values = sequenceOf(PreviewParameterData.ledger)
|
||||
}
|
||||
|
@ -74,13 +74,16 @@ private fun walletUiState(
|
||||
is Result.Success -> {
|
||||
val config = result.data.first
|
||||
val ledger = result.data.second
|
||||
val accountBalance = List(ledger.indexedAccounts.size) { index ->
|
||||
index to ledger.getBalanceForAccount(index)
|
||||
}.toMap()
|
||||
val addresses =
|
||||
ledger.accountAddresses.groupBy { it.accountIndex }.flatMap { (_, group) ->
|
||||
group.sortedBy { it.subAddressIndex }.mapIndexed { index, address ->
|
||||
ledger.indexedAccounts.flatMap { account ->
|
||||
account.addresses.map { address ->
|
||||
WalletAddress(
|
||||
address = address,
|
||||
used = address.isAddressUsed(ledger.transactions),
|
||||
isLastForAccount = index == group.size - 1,
|
||||
isLastForAccount = address === account.addresses.last(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -91,7 +94,8 @@ private fun walletUiState(
|
||||
config = config,
|
||||
network = ledger.publicAddress.network,
|
||||
blockchainTime = ledger.checkedAt,
|
||||
balance = ledger.balance,
|
||||
totalBalance = ledger.getBalance(),
|
||||
accountBalance = accountBalance,
|
||||
addresses = addresses,
|
||||
transactions = transactions,
|
||||
)
|
||||
@ -113,7 +117,8 @@ sealed interface WalletUiState {
|
||||
val config: WalletConfig,
|
||||
val network: MoneroNetwork,
|
||||
val blockchainTime: BlockchainTime,
|
||||
val balance: Balance,
|
||||
val totalBalance: Balance,
|
||||
val accountBalance: Map<Int, Balance>,
|
||||
val addresses: List<WalletAddress>,
|
||||
val transactions: List<WalletTransaction>,
|
||||
) : WalletUiState
|
||||
|
@ -7,12 +7,13 @@ import androidx.compose.ui.Modifier
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SelectListBox(
|
||||
fun <T>SelectListBox(
|
||||
label: String,
|
||||
options: List<String>,
|
||||
selectedOption: String,
|
||||
onOptionClick: (String) -> Unit,
|
||||
options: Map<T, String>,
|
||||
selectedOption: T,
|
||||
onOptionClick: (T) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
@ -25,16 +26,19 @@ fun SelectListBox(
|
||||
) {
|
||||
OutlinedTextField(
|
||||
readOnly = true,
|
||||
value = selectedOption,
|
||||
value = options.getValue(selectedOption),
|
||||
onValueChange = { },
|
||||
enabled = enabled,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
label = { Text(label) },
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(
|
||||
expanded = expanded
|
||||
)
|
||||
if (enabled) {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(
|
||||
expanded = expanded
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
||||
)
|
||||
@ -45,13 +49,13 @@ fun SelectListBox(
|
||||
},
|
||||
modifier = Modifier.exposedDropdownSize(),
|
||||
) {
|
||||
options.forEach { selectionOption ->
|
||||
options.forEach { (key, text) ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(selectionOption)
|
||||
Text(text)
|
||||
},
|
||||
onClick = {
|
||||
onOptionClick(selectionOption)
|
||||
onOptionClick(key)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
|
@ -40,9 +40,9 @@ object PreviewParameterData {
|
||||
|
||||
val ledger = Ledger(
|
||||
publicAddress = PublicAddress.parse("4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T"),
|
||||
accountAddresses = emptySet(),
|
||||
indexedAccounts = emptyList(),
|
||||
transactionById = transactions.associateBy { it.txId },
|
||||
enotes = emptySet(),
|
||||
enoteSet = emptySet(),
|
||||
checkedAt = BlockchainTime(blockHeader = blockHeader, network = network),
|
||||
)
|
||||
}
|
||||
|
@ -19,5 +19,5 @@ oneway interface ITransferRequestCallback {
|
||||
// void onTransactionTooBig();
|
||||
// void onTransferError(String errorMessage);
|
||||
// void onWalletInternalError(String errorMessage);
|
||||
// void onUnexpectedError(String errorMessage);
|
||||
void onUnexpectedError(String message);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ jmethodID HttpResponse_getBody;
|
||||
jmethodID HttpResponse_getCode;
|
||||
jmethodID HttpResponse_getContentType;
|
||||
jmethodID ITransferRequestCb_onTransferCreated;
|
||||
jmethodID ITransferRequestCb_onUnexpectedError;
|
||||
jmethodID Logger_logFromNative;
|
||||
jmethodID TxInfo_ctor;
|
||||
jmethodID WalletNative_createPendingTransfer;
|
||||
@ -41,6 +42,9 @@ void InitializeJniCache(JNIEnv* env) {
|
||||
ITransferRequestCb_onTransferCreated = GetMethodId(
|
||||
env, iTransferRequestCb,
|
||||
"onTransferCreated", "(Lim/molly/monero/IPendingTransfer;)V");
|
||||
ITransferRequestCb_onUnexpectedError = GetMethodId(
|
||||
env, iTransferRequestCb,
|
||||
"onUnexpectedError", "(Ljava/lang/String;)V");
|
||||
Logger_logFromNative = GetMethodId(
|
||||
env, logger,
|
||||
"logFromNative", "(ILjava/lang/String;Ljava/lang/String;)V");
|
||||
|
@ -14,6 +14,7 @@ extern jmethodID HttpResponse_getBody;
|
||||
extern jmethodID HttpResponse_getCode;
|
||||
extern jmethodID HttpResponse_getContentType;
|
||||
extern jmethodID ITransferRequestCb_onTransferCreated;
|
||||
extern jmethodID ITransferRequestCb_onUnexpectedError;
|
||||
extern jmethodID Logger_logFromNative;
|
||||
extern jmethodID TxInfo_ctor;
|
||||
extern jmethodID WalletNative_callRemoteNode;
|
||||
|
@ -874,9 +874,12 @@ Java_im_molly_monero_WalletNative_nativeCreatePayment(
|
||||
// } catch (error::transfer_error& e) {
|
||||
// } catch (error::wallet_internal_error& e) {
|
||||
// } catch (error::wallet_logic_error& e) {
|
||||
// } catch (const std::exception& e) {
|
||||
} catch (...) {
|
||||
LOG_FATAL("Caught unknown exception");
|
||||
} catch (const std::exception& e) {
|
||||
LOGW("Caught unhandled exception: %s", e.what());
|
||||
CallVoidMethod(env, j_callback,
|
||||
ITransferRequestCb_onUnexpectedError,
|
||||
NativeToJavaString(env, e.what()));
|
||||
return;
|
||||
}
|
||||
|
||||
jobject j_pending_transfer = CallObjectMethod(
|
||||
|
@ -30,6 +30,10 @@ data class BlockchainTime(
|
||||
}
|
||||
}
|
||||
|
||||
fun withTimestamp(newTimestamp: Instant): BlockchainTime {
|
||||
return copy(timestamp = newTimestamp)
|
||||
}
|
||||
|
||||
fun estimateHeight(targetTimestamp: Instant): Int {
|
||||
val timeDiff = Duration.between(timestamp, targetTimestamp)
|
||||
val estHeight = timeDiff.seconds / network.avgBlockTime(height).seconds + height
|
||||
|
@ -4,14 +4,15 @@ import im.molly.monero.internal.TxInfo
|
||||
import im.molly.monero.internal.consolidateTransactions
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.onFailure
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@ -191,6 +192,12 @@ class MoneroWallet internal constructor(
|
||||
pendingTransfer.close()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUnexpectedError(message: String) {
|
||||
continuation.resumeWithException(
|
||||
IllegalStateException(message)
|
||||
)
|
||||
}
|
||||
}
|
||||
when (transferRequest) {
|
||||
is PaymentRequest -> wallet.createPayment(transferRequest, callback)
|
||||
|
@ -8,7 +8,7 @@ sealed interface TransferRequest : Parcelable
|
||||
@Parcelize
|
||||
data class PaymentRequest(
|
||||
val paymentDetails: List<PaymentDetail>,
|
||||
val sourceAccounts: Set<AccountAddress>,
|
||||
val spendingAccountIndex: Int,
|
||||
val feePriority: FeePriority? = null,
|
||||
val timeLock: UnlockTime? = null,
|
||||
) : TransferRequest
|
||||
|
@ -187,7 +187,7 @@ internal class WalletNative private constructor(
|
||||
amounts = amounts.toLongArray(),
|
||||
timeLock = request.timeLock?.blockchainTime?.toLong() ?: 0,
|
||||
priority = request.feePriority?.priority ?: 0,
|
||||
accountIndex = 0,
|
||||
accountIndex = request.spendingAccountIndex,
|
||||
subAddressIndexes = IntArray(0),
|
||||
callback = callback,
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user