mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2025-02-25 17:01:15 -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?> =
|
fun getTransaction(walletId: Long, txId: String): Flow<Transaction?> =
|
||||||
getLedger(walletId).map { it.transactionById[txId] }
|
getLedger(walletId).map { it.transactionById[txId] }
|
||||||
|
|
||||||
|
suspend fun createTransfer(walletId: Long, transferRequest: TransferRequest): PendingTransfer {
|
||||||
|
return getWallet(walletId).createTransfer(transferRequest)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun addWallet(
|
suspend fun addWallet(
|
||||||
moneroNetwork: MoneroNetwork,
|
moneroNetwork: MoneroNetwork,
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -170,10 +170,10 @@ private fun SecondStepScreen(
|
|||||||
)
|
)
|
||||||
SelectListBox(
|
SelectListBox(
|
||||||
label = "Network",
|
label = "Network",
|
||||||
options = MoneroNetwork.values().map { it.name },
|
options = MoneroNetwork.entries.associateWith { it.name },
|
||||||
selectedOption = network.name,
|
selectedOption = network,
|
||||||
onOptionClick = {
|
onOptionClick = {
|
||||||
onNetworkChanged(MoneroNetwork.valueOf(it))
|
onNetworkChanged(it)
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
@ -7,14 +7,11 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import im.molly.monero.demo.data.model.WalletAddress
|
import im.molly.monero.demo.data.model.WalletAddress
|
||||||
import im.molly.monero.demo.ui.component.CopyableText
|
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
|
@Composable
|
||||||
fun AddressCardExpanded(
|
fun AddressCardExpanded(
|
||||||
@ -47,10 +44,6 @@ fun AddressCardExpanded(
|
|||||||
modifier = if (used) Modifier.alpha(0.5f) else Modifier,
|
modifier = if (used) Modifier.alpha(0.5f) else Modifier,
|
||||||
)
|
)
|
||||||
if (walletAddress.isLastForAccount) {
|
if (walletAddress.isLastForAccount) {
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
TextButton(onClick = onCreateSubAddressClick) {
|
TextButton(onClick = onCreateSubAddressClick) {
|
||||||
Text(
|
Text(
|
||||||
text = "Add subaddress",
|
text = "Add subaddress",
|
||||||
@ -58,7 +51,7 @@ fun AddressCardExpanded(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,10 +67,10 @@ private fun EditRemoteNodeDialog(
|
|||||||
)
|
)
|
||||||
SelectListBox(
|
SelectListBox(
|
||||||
label = "Network",
|
label = "Network",
|
||||||
options = MoneroNetwork.values().map { it.name },
|
options = MoneroNetwork.entries.associateWith { it.name },
|
||||||
selectedOption = remoteNode.network.name,
|
selectedOption = remoteNode.network,
|
||||||
onOptionClick = {
|
onOptionClick = {
|
||||||
onRemoteNodeChange(remoteNode.copy(network = MoneroNetwork.valueOf(it)))
|
onRemoteNodeChange(remoteNode.copy(network = it))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Column {
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -55,17 +55,17 @@ fun WalletBalanceView(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
text = "Balance at ${blockchainTime}",
|
text = "Balance at $blockchainTime",
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
BalanceRow("Confirmed", balance.confirmedAmount)
|
BalanceRow("Confirmed", balance.confirmedAmount)
|
||||||
BalanceRow("Pending", balance.pendingAmount)
|
BalanceRow("Pending", balance.pendingAmount)
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
BalanceRow("Total", balance.totalAmount)
|
BalanceRow("Total", balance.totalAmount)
|
||||||
|
|
||||||
val currentTime = blockchainTime.copy(timestamp = now)
|
val currentTime = blockchainTime.withTimestamp(now)
|
||||||
|
|
||||||
BalanceRow("Unlocked", balance.unlockedAmountAt(currentTime))
|
BalanceRow("Unlocked", balance.unlockedAmountAt(currentTime))
|
||||||
balance.lockedAmountsAt(currentTime).forEach { (timeSpan, amount) ->
|
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.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
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.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
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.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import im.molly.monero.FeePriority
|
||||||
import im.molly.monero.Ledger
|
import im.molly.monero.Ledger
|
||||||
import im.molly.monero.MoneroCurrency
|
import im.molly.monero.MoneroCurrency
|
||||||
import im.molly.monero.demo.data.model.WalletConfig
|
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.component.Toolbar
|
||||||
import im.molly.monero.demo.ui.preview.PreviewParameterData
|
import im.molly.monero.demo.ui.preview.PreviewParameterData
|
||||||
import im.molly.monero.demo.ui.theme.AppIcons
|
import im.molly.monero.demo.ui.theme.AppIcons
|
||||||
import im.molly.monero.demo.ui.theme.AppTheme
|
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
|
@Composable
|
||||||
fun WalletRoute(
|
fun WalletRoute(
|
||||||
@ -34,20 +43,32 @@ fun WalletRoute(
|
|||||||
onTransactionClick: (String, Long) -> Unit,
|
onTransactionClick: (String, Long) -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: WalletViewModel = viewModel(
|
walletViewModel: WalletViewModel = viewModel(
|
||||||
factory = WalletViewModel.factory(walletId),
|
factory = WalletViewModel.factory(walletId),
|
||||||
key = WalletViewModel.key(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(
|
WalletScreen(
|
||||||
uiState = uiState,
|
walletUiState = walletUiState,
|
||||||
onWalletConfigChange = { config -> viewModel.updateConfig(config) },
|
sendTabUiState = sendTabUiState,
|
||||||
onTransactionClick = onTransactionClick,
|
onWalletConfigChange = { config ->
|
||||||
onCreateAccountClick = { viewModel.createAccount() },
|
walletViewModel.updateConfig(config)
|
||||||
onCreateSubAddressClick = { accountIndex ->
|
|
||||||
viewModel.createSubAddress(accountIndex)
|
|
||||||
},
|
},
|
||||||
|
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,
|
onBackClick = onBackClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
@ -55,21 +76,31 @@ fun WalletRoute(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun WalletScreen(
|
private fun WalletScreen(
|
||||||
uiState: WalletUiState,
|
walletUiState: WalletUiState,
|
||||||
onWalletConfigChange: (WalletConfig) -> Unit,
|
sendTabUiState: SendTabUiState,
|
||||||
onTransactionClick: (String, Long) -> Unit,
|
|
||||||
onCreateAccountClick: () -> Unit,
|
|
||||||
onCreateSubAddressClick: (Int) -> Unit,
|
|
||||||
onBackClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
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(
|
is WalletUiState.Loaded -> WalletScreenLoaded(
|
||||||
uiState = uiState,
|
walletUiState = walletUiState,
|
||||||
|
sendTabUiState = sendTabUiState,
|
||||||
onWalletConfigChange = onWalletConfigChange,
|
onWalletConfigChange = onWalletConfigChange,
|
||||||
onTransactionClick = onTransactionClick,
|
onTransactionClick = onTransactionClick,
|
||||||
onCreateAccountClick = onCreateAccountClick,
|
onCreateAccountClick = onCreateAccountClick,
|
||||||
onCreateSubAddressClick = onCreateSubAddressClick,
|
onCreateSubAddressClick = onCreateSubAddressClick,
|
||||||
|
onTransferAccountSelect = onTransferAccountSelect,
|
||||||
|
onTransferPrioritySelect = onTransferPrioritySelect,
|
||||||
|
onTransferRecipientChange = onTransferRecipientChange,
|
||||||
|
onTransferSendClick = onTransferSendClick,
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
@ -80,13 +111,33 @@ private fun WalletScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@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
|
@Composable
|
||||||
private fun WalletScreenLoaded(
|
private fun WalletScreenLoaded(
|
||||||
uiState: WalletUiState.Loaded,
|
walletUiState: WalletUiState.Loaded,
|
||||||
|
sendTabUiState: SendTabUiState,
|
||||||
onWalletConfigChange: (WalletConfig) -> Unit,
|
onWalletConfigChange: (WalletConfig) -> Unit,
|
||||||
onTransactionClick: (String, Long) -> Unit,
|
onTransactionClick: (String, Long) -> Unit,
|
||||||
onCreateAccountClick: () -> Unit,
|
onCreateAccountClick: () -> Unit,
|
||||||
onCreateSubAddressClick: (Int) -> Unit,
|
onCreateSubAddressClick: (Int) -> Unit,
|
||||||
|
onTransferAccountSelect: (Int) -> Unit,
|
||||||
|
onTransferPrioritySelect: (FeePriority) -> Unit,
|
||||||
|
onTransferRecipientChange: (List<Pair<String, String>>) -> Unit,
|
||||||
|
onTransferSendClick: () -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@ -96,14 +147,7 @@ private fun WalletScreenLoaded(
|
|||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
Toolbar(navigationIcon = {
|
WalletToolbar(onBackClick = onBackClick, actions = {
|
||||||
IconButton(onClick = onBackClick) {
|
|
||||||
Icon(
|
|
||||||
imageVector = AppIcons.ArrowBack,
|
|
||||||
contentDescription = "Back",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, actions = {
|
|
||||||
WalletKebabMenu(
|
WalletKebabMenu(
|
||||||
onRenameClick = { showRenameDialog = true },
|
onRenameClick = { showRenameDialog = true },
|
||||||
onDeleteClick = { },
|
onDeleteClick = { },
|
||||||
@ -122,11 +166,11 @@ private fun WalletScreenLoaded(
|
|||||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||||
append(
|
append(
|
||||||
MoneroCurrency.Format(precision = 5)
|
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(
|
WalletHeaderTabs(
|
||||||
titles = listOf("Balance", "Send", "Receive", "History"),
|
titles = listOf("Balance", "Send", "Receive", "History"),
|
||||||
@ -137,12 +181,20 @@ private fun WalletScreenLoaded(
|
|||||||
when (selectedTabIndex) {
|
when (selectedTabIndex) {
|
||||||
0 -> {
|
0 -> {
|
||||||
WalletBalanceView(
|
WalletBalanceView(
|
||||||
balance = uiState.balance,
|
balance = walletUiState.totalBalance,
|
||||||
blockchainTime = uiState.blockchainTime
|
blockchainTime = walletUiState.blockchainTime
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
1 -> {} // TODO
|
1 -> {
|
||||||
|
WalletSendTab(
|
||||||
|
walletUiState, sendTabUiState,
|
||||||
|
onAccountSelect = onTransferAccountSelect,
|
||||||
|
onPrioritySelect = onTransferPrioritySelect,
|
||||||
|
onRecipientChange = onTransferRecipientChange,
|
||||||
|
onSendClick = onTransferSendClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
2 -> {
|
2 -> {
|
||||||
val scrollState = rememberLazyListState()
|
val scrollState = rememberLazyListState()
|
||||||
@ -151,17 +203,13 @@ private fun WalletScreenLoaded(
|
|||||||
state = scrollState,
|
state = scrollState,
|
||||||
) {
|
) {
|
||||||
addressCardItems(
|
addressCardItems(
|
||||||
items = uiState.addresses,
|
items = walletUiState.addresses,
|
||||||
onCreateSubAddressClick = onCreateSubAddressClick,
|
onCreateSubAddressClick = onCreateSubAddressClick,
|
||||||
)
|
)
|
||||||
item {
|
item {
|
||||||
Column(
|
TextButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = onCreateAccountClick,
|
onClick = onCreateAccountClick,
|
||||||
modifier = modifier.padding(bottom = 16.dp),
|
modifier = modifier.padding(start = 16.dp, bottom = 8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Create new account",
|
text = "Create new account",
|
||||||
@ -171,16 +219,15 @@ private fun WalletScreenLoaded(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
3 -> {
|
3 -> {
|
||||||
val scrollState = rememberLazyListState()
|
val scrollState = rememberLazyListState()
|
||||||
|
|
||||||
var lastTxId by remember { mutableStateOf("") }
|
var lastTxId by remember { mutableStateOf("") }
|
||||||
lastTxId = uiState.transactions.firstOrNull()?.transaction?.txId ?: ""
|
lastTxId = walletUiState.transactions.firstOrNull()?.transaction?.txId ?: ""
|
||||||
|
|
||||||
LaunchedEffect(lastTxId) {
|
LaunchedEffect(lastTxId) {
|
||||||
if (uiState.transactions.isNotEmpty()) {
|
if (walletUiState.transactions.isNotEmpty()) {
|
||||||
scrollState.scrollToItem(0)
|
scrollState.scrollToItem(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -189,7 +236,7 @@ private fun WalletScreenLoaded(
|
|||||||
state = scrollState,
|
state = scrollState,
|
||||||
) {
|
) {
|
||||||
transactionCardItems(
|
transactionCardItems(
|
||||||
items = uiState.transactions,
|
items = walletUiState.transactions,
|
||||||
onTransactionClick = onTransactionClick,
|
onTransactionClick = onTransactionClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -198,7 +245,7 @@ private fun WalletScreenLoaded(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showRenameDialog) {
|
if (showRenameDialog) {
|
||||||
var name by remember { mutableStateOf(uiState.config.name) }
|
var name by remember { mutableStateOf(walletUiState.config.name) }
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showRenameDialog = false },
|
onDismissRequest = { showRenameDialog = false },
|
||||||
title = { Text("Enter wallet name") },
|
title = { Text("Enter wallet name") },
|
||||||
@ -211,7 +258,7 @@ private fun WalletScreenLoaded(
|
|||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onWalletConfigChange(uiState.config.copy(name = name))
|
onWalletConfigChange(walletUiState.config.copy(name = name))
|
||||||
showRenameDialog = false
|
showRenameDialog = false
|
||||||
}) {
|
}) {
|
||||||
Text("Rename")
|
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
|
@Composable
|
||||||
private fun WalletScreenError(
|
private fun WalletScreenError(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
Scaffold(topBar = { WalletToolbar(onBackClick = onBackClick) }) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun WalletScreenLoading(
|
private fun WalletScreenLoading(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
Scaffold(topBar = { WalletToolbar(onBackClick = onBackClick) }) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -300,7 +431,7 @@ private fun WalletScreenPopulated(
|
|||||||
) {
|
) {
|
||||||
AppTheme {
|
AppTheme {
|
||||||
WalletScreen(
|
WalletScreen(
|
||||||
uiState = WalletUiState.Loaded(
|
walletUiState = WalletUiState.Loaded(
|
||||||
config = WalletConfig(
|
config = WalletConfig(
|
||||||
id = 0,
|
id = 0,
|
||||||
publicAddress = ledger.publicAddress.address,
|
publicAddress = ledger.publicAddress.address,
|
||||||
@ -309,21 +440,21 @@ private fun WalletScreenPopulated(
|
|||||||
remoteNodes = emptySet(),
|
remoteNodes = emptySet(),
|
||||||
),
|
),
|
||||||
network = ledger.publicAddress.network,
|
network = ledger.publicAddress.network,
|
||||||
balance = ledger.balance,
|
totalBalance = ledger.getBalance(),
|
||||||
|
accountBalance = emptyMap(),
|
||||||
blockchainTime = ledger.checkedAt,
|
blockchainTime = ledger.checkedAt,
|
||||||
addresses = emptyList(),
|
addresses = emptyList(),
|
||||||
transactions = emptyList(),
|
transactions = emptyList(),
|
||||||
),
|
),
|
||||||
onWalletConfigChange = {},
|
sendTabUiState = SendTabUiState(
|
||||||
onTransactionClick = { _: String, _: Long -> },
|
accountIndex = 0,
|
||||||
onCreateAccountClick = {},
|
recipients = emptyList(),
|
||||||
onCreateSubAddressClick = {},
|
feePriority = FeePriority.Medium,
|
||||||
onBackClick = {},
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class WalletScreenPreviewParameterProvider :
|
private class WalletScreenPreviewParameterProvider : PreviewParameterProvider<Ledger> {
|
||||||
PreviewParameterProvider<Ledger> {
|
|
||||||
override val values = sequenceOf(PreviewParameterData.ledger)
|
override val values = sequenceOf(PreviewParameterData.ledger)
|
||||||
}
|
}
|
||||||
|
@ -74,13 +74,16 @@ private fun walletUiState(
|
|||||||
is Result.Success -> {
|
is Result.Success -> {
|
||||||
val config = result.data.first
|
val config = result.data.first
|
||||||
val ledger = result.data.second
|
val ledger = result.data.second
|
||||||
|
val accountBalance = List(ledger.indexedAccounts.size) { index ->
|
||||||
|
index to ledger.getBalanceForAccount(index)
|
||||||
|
}.toMap()
|
||||||
val addresses =
|
val addresses =
|
||||||
ledger.accountAddresses.groupBy { it.accountIndex }.flatMap { (_, group) ->
|
ledger.indexedAccounts.flatMap { account ->
|
||||||
group.sortedBy { it.subAddressIndex }.mapIndexed { index, address ->
|
account.addresses.map { address ->
|
||||||
WalletAddress(
|
WalletAddress(
|
||||||
address = address,
|
address = address,
|
||||||
used = address.isAddressUsed(ledger.transactions),
|
used = address.isAddressUsed(ledger.transactions),
|
||||||
isLastForAccount = index == group.size - 1,
|
isLastForAccount = address === account.addresses.last(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,7 +94,8 @@ private fun walletUiState(
|
|||||||
config = config,
|
config = config,
|
||||||
network = ledger.publicAddress.network,
|
network = ledger.publicAddress.network,
|
||||||
blockchainTime = ledger.checkedAt,
|
blockchainTime = ledger.checkedAt,
|
||||||
balance = ledger.balance,
|
totalBalance = ledger.getBalance(),
|
||||||
|
accountBalance = accountBalance,
|
||||||
addresses = addresses,
|
addresses = addresses,
|
||||||
transactions = transactions,
|
transactions = transactions,
|
||||||
)
|
)
|
||||||
@ -113,7 +117,8 @@ sealed interface WalletUiState {
|
|||||||
val config: WalletConfig,
|
val config: WalletConfig,
|
||||||
val network: MoneroNetwork,
|
val network: MoneroNetwork,
|
||||||
val blockchainTime: BlockchainTime,
|
val blockchainTime: BlockchainTime,
|
||||||
val balance: Balance,
|
val totalBalance: Balance,
|
||||||
|
val accountBalance: Map<Int, Balance>,
|
||||||
val addresses: List<WalletAddress>,
|
val addresses: List<WalletAddress>,
|
||||||
val transactions: List<WalletTransaction>,
|
val transactions: List<WalletTransaction>,
|
||||||
) : WalletUiState
|
) : WalletUiState
|
||||||
|
@ -7,12 +7,13 @@ import androidx.compose.ui.Modifier
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectListBox(
|
fun <T>SelectListBox(
|
||||||
label: String,
|
label: String,
|
||||||
options: List<String>,
|
options: Map<T, String>,
|
||||||
selectedOption: String,
|
selectedOption: T,
|
||||||
onOptionClick: (String) -> Unit,
|
onOptionClick: (T) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
) {
|
) {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@ -25,16 +26,19 @@ fun SelectListBox(
|
|||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
value = selectedOption,
|
value = options.getValue(selectedOption),
|
||||||
onValueChange = { },
|
onValueChange = { },
|
||||||
|
enabled = enabled,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.menuAnchor(),
|
.menuAnchor(),
|
||||||
label = { Text(label) },
|
label = { Text(label) },
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
|
if (enabled) {
|
||||||
ExposedDropdownMenuDefaults.TrailingIcon(
|
ExposedDropdownMenuDefaults.TrailingIcon(
|
||||||
expanded = expanded
|
expanded = expanded
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
||||||
)
|
)
|
||||||
@ -45,13 +49,13 @@ fun SelectListBox(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.exposedDropdownSize(),
|
modifier = Modifier.exposedDropdownSize(),
|
||||||
) {
|
) {
|
||||||
options.forEach { selectionOption ->
|
options.forEach { (key, text) ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Text(selectionOption)
|
Text(text)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
onOptionClick(selectionOption)
|
onOptionClick(key)
|
||||||
expanded = false
|
expanded = false
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -40,9 +40,9 @@ object PreviewParameterData {
|
|||||||
|
|
||||||
val ledger = Ledger(
|
val ledger = Ledger(
|
||||||
publicAddress = PublicAddress.parse("4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T"),
|
publicAddress = PublicAddress.parse("4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T"),
|
||||||
accountAddresses = emptySet(),
|
indexedAccounts = emptyList(),
|
||||||
transactionById = transactions.associateBy { it.txId },
|
transactionById = transactions.associateBy { it.txId },
|
||||||
enotes = emptySet(),
|
enoteSet = emptySet(),
|
||||||
checkedAt = BlockchainTime(blockHeader = blockHeader, network = network),
|
checkedAt = BlockchainTime(blockHeader = blockHeader, network = network),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -19,5 +19,5 @@ oneway interface ITransferRequestCallback {
|
|||||||
// void onTransactionTooBig();
|
// void onTransactionTooBig();
|
||||||
// void onTransferError(String errorMessage);
|
// void onTransferError(String errorMessage);
|
||||||
// void onWalletInternalError(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_getCode;
|
||||||
jmethodID HttpResponse_getContentType;
|
jmethodID HttpResponse_getContentType;
|
||||||
jmethodID ITransferRequestCb_onTransferCreated;
|
jmethodID ITransferRequestCb_onTransferCreated;
|
||||||
|
jmethodID ITransferRequestCb_onUnexpectedError;
|
||||||
jmethodID Logger_logFromNative;
|
jmethodID Logger_logFromNative;
|
||||||
jmethodID TxInfo_ctor;
|
jmethodID TxInfo_ctor;
|
||||||
jmethodID WalletNative_createPendingTransfer;
|
jmethodID WalletNative_createPendingTransfer;
|
||||||
@ -41,6 +42,9 @@ void InitializeJniCache(JNIEnv* env) {
|
|||||||
ITransferRequestCb_onTransferCreated = GetMethodId(
|
ITransferRequestCb_onTransferCreated = GetMethodId(
|
||||||
env, iTransferRequestCb,
|
env, iTransferRequestCb,
|
||||||
"onTransferCreated", "(Lim/molly/monero/IPendingTransfer;)V");
|
"onTransferCreated", "(Lim/molly/monero/IPendingTransfer;)V");
|
||||||
|
ITransferRequestCb_onUnexpectedError = GetMethodId(
|
||||||
|
env, iTransferRequestCb,
|
||||||
|
"onUnexpectedError", "(Ljava/lang/String;)V");
|
||||||
Logger_logFromNative = GetMethodId(
|
Logger_logFromNative = GetMethodId(
|
||||||
env, logger,
|
env, logger,
|
||||||
"logFromNative", "(ILjava/lang/String;Ljava/lang/String;)V");
|
"logFromNative", "(ILjava/lang/String;Ljava/lang/String;)V");
|
||||||
|
@ -14,6 +14,7 @@ extern jmethodID HttpResponse_getBody;
|
|||||||
extern jmethodID HttpResponse_getCode;
|
extern jmethodID HttpResponse_getCode;
|
||||||
extern jmethodID HttpResponse_getContentType;
|
extern jmethodID HttpResponse_getContentType;
|
||||||
extern jmethodID ITransferRequestCb_onTransferCreated;
|
extern jmethodID ITransferRequestCb_onTransferCreated;
|
||||||
|
extern jmethodID ITransferRequestCb_onUnexpectedError;
|
||||||
extern jmethodID Logger_logFromNative;
|
extern jmethodID Logger_logFromNative;
|
||||||
extern jmethodID TxInfo_ctor;
|
extern jmethodID TxInfo_ctor;
|
||||||
extern jmethodID WalletNative_callRemoteNode;
|
extern jmethodID WalletNative_callRemoteNode;
|
||||||
|
@ -874,9 +874,12 @@ Java_im_molly_monero_WalletNative_nativeCreatePayment(
|
|||||||
// } catch (error::transfer_error& e) {
|
// } catch (error::transfer_error& e) {
|
||||||
// } catch (error::wallet_internal_error& e) {
|
// } catch (error::wallet_internal_error& e) {
|
||||||
// } catch (error::wallet_logic_error& e) {
|
// } catch (error::wallet_logic_error& e) {
|
||||||
// } catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
} catch (...) {
|
LOGW("Caught unhandled exception: %s", e.what());
|
||||||
LOG_FATAL("Caught unknown exception");
|
CallVoidMethod(env, j_callback,
|
||||||
|
ITransferRequestCb_onUnexpectedError,
|
||||||
|
NativeToJavaString(env, e.what()));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
jobject j_pending_transfer = CallObjectMethod(
|
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 {
|
fun estimateHeight(targetTimestamp: Instant): Int {
|
||||||
val timeDiff = Duration.between(timestamp, targetTimestamp)
|
val timeDiff = Duration.between(timestamp, targetTimestamp)
|
||||||
val estHeight = timeDiff.seconds / network.avgBlockTime(height).seconds + height
|
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 im.molly.monero.internal.consolidateTransactions
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.channels.onFailure
|
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.conflate
|
import kotlinx.coroutines.flow.conflate
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@ -191,6 +192,12 @@ class MoneroWallet internal constructor(
|
|||||||
pendingTransfer.close()
|
pendingTransfer.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onUnexpectedError(message: String) {
|
||||||
|
continuation.resumeWithException(
|
||||||
|
IllegalStateException(message)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
when (transferRequest) {
|
when (transferRequest) {
|
||||||
is PaymentRequest -> wallet.createPayment(transferRequest, callback)
|
is PaymentRequest -> wallet.createPayment(transferRequest, callback)
|
||||||
|
@ -8,7 +8,7 @@ sealed interface TransferRequest : Parcelable
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
data class PaymentRequest(
|
data class PaymentRequest(
|
||||||
val paymentDetails: List<PaymentDetail>,
|
val paymentDetails: List<PaymentDetail>,
|
||||||
val sourceAccounts: Set<AccountAddress>,
|
val spendingAccountIndex: Int,
|
||||||
val feePriority: FeePriority? = null,
|
val feePriority: FeePriority? = null,
|
||||||
val timeLock: UnlockTime? = null,
|
val timeLock: UnlockTime? = null,
|
||||||
) : TransferRequest
|
) : TransferRequest
|
||||||
|
@ -187,7 +187,7 @@ internal class WalletNative private constructor(
|
|||||||
amounts = amounts.toLongArray(),
|
amounts = amounts.toLongArray(),
|
||||||
timeLock = request.timeLock?.blockchainTime?.toLong() ?: 0,
|
timeLock = request.timeLock?.blockchainTime?.toLong() ?: 0,
|
||||||
priority = request.feePriority?.priority ?: 0,
|
priority = request.feePriority?.priority ?: 0,
|
||||||
accountIndex = 0,
|
accountIndex = request.spendingAccountIndex,
|
||||||
subAddressIndexes = IntArray(0),
|
subAddressIndexes = IntArray(0),
|
||||||
callback = callback,
|
callback = callback,
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user