mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2024-12-25 07:29:23 -05:00
demo: transaction screen and components
This commit is contained in:
parent
a84375d384
commit
42cf100951
@ -58,6 +58,9 @@ class WalletRepository(
|
||||
emitAll(getWallet(walletId).ledger())
|
||||
}
|
||||
|
||||
fun getTransaction(walletId: Long, txId: String): Flow<Transaction?> =
|
||||
getLedger(walletId).map { it.transactions[txId] }
|
||||
|
||||
suspend fun addWallet(
|
||||
moneroNetwork: MoneroNetwork,
|
||||
name: String,
|
||||
|
@ -1,9 +0,0 @@
|
||||
package im.molly.monero.demo.data.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class Transaction(
|
||||
@PrimaryKey val uid: Int,
|
||||
)
|
@ -0,0 +1,8 @@
|
||||
package im.molly.monero.demo.data.model
|
||||
|
||||
import im.molly.monero.Transaction
|
||||
|
||||
data class WalletTransaction(
|
||||
val walletId: Long,
|
||||
val transaction: Transaction,
|
||||
)
|
@ -1,20 +1,37 @@
|
||||
package im.molly.monero.demo.ui
|
||||
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import im.molly.monero.demo.ui.component.Toolbar
|
||||
|
||||
@Composable
|
||||
fun HistoryRoute(
|
||||
modifier: Modifier = Modifier,
|
||||
navigateToTransaction: (String, Long) -> Unit,
|
||||
) {
|
||||
HistoryScreen(
|
||||
modifier = modifier,
|
||||
onTransactionClick = navigateToTransaction,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun HistoryScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onTransactionClick: (String, Long) -> Unit,
|
||||
) {
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
Toolbar(
|
||||
title = "Transaction history",
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
@ -18,9 +18,9 @@ import im.molly.monero.demo.ui.component.Toolbar
|
||||
fun HomeRoute(
|
||||
navigateToAddWalletWizard: () -> Unit,
|
||||
navigateToWallet: (Long) -> Unit,
|
||||
viewModel: HomeViewModel = viewModel(),
|
||||
walletListViewModel: WalletListViewModel = viewModel(),
|
||||
) {
|
||||
val walletListUiState: WalletListUiState by viewModel.walletListUiState.collectAsStateWithLifecycle()
|
||||
val walletListUiState by walletListViewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
HomeScreen(
|
||||
walletListUiState = walletListUiState,
|
||||
@ -56,8 +56,8 @@ private fun HomeScreen(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding))
|
||||
{
|
||||
.padding(padding),
|
||||
) {
|
||||
walletCards(walletListUiState, onWalletClick)
|
||||
}
|
||||
}
|
||||
@ -71,8 +71,11 @@ private fun LazyListScope.walletCards(
|
||||
WalletListUiState.Loading -> item {
|
||||
Text(text = "Loading wallet list...") // TODO
|
||||
}
|
||||
is WalletListUiState.Success -> {
|
||||
walletCardsItems(walletListUiState.ids, onWalletClick)
|
||||
|
||||
is WalletListUiState.Loaded -> {
|
||||
walletCardItems(walletListUiState.walletIds, onWalletClick)
|
||||
}
|
||||
|
||||
is WalletListUiState.Empty -> Unit // TODO
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
package im.molly.monero.demo.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import im.molly.monero.demo.AppModule
|
||||
import im.molly.monero.demo.data.WalletRepository
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
class HomeViewModel(
|
||||
private val walletRepository: WalletRepository = AppModule.walletRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
val walletListUiState: StateFlow<WalletListUiState> = walletRepository.getWalletIdList()
|
||||
.map { WalletListUiState.Success(it) }
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = WalletListUiState.Loading,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
sealed interface HomeUiState {
|
||||
data object Loading : HomeUiState
|
||||
|
||||
// data class Ready(
|
||||
// val wallets: List<WalletDetails>
|
||||
// ) : HomeUiState
|
||||
|
||||
data object Empty : HomeUiState
|
||||
}
|
||||
|
||||
sealed interface WalletListUiState {
|
||||
data class Success(val ids: List<Long>) : WalletListUiState
|
||||
data object Loading : WalletListUiState
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.molly.monero.MoneroCurrency
|
||||
import im.molly.monero.Transaction
|
||||
import im.molly.monero.demo.ui.preview.PreviewParameterData
|
||||
import im.molly.monero.demo.ui.theme.AppTheme
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TransactionCardExpanded(
|
||||
transaction: Transaction,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Card(
|
||||
onClick = onClick,
|
||||
modifier = modifier.padding(top = 8.dp, start = 8.dp, end = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
) {
|
||||
TransactionDetail("State", transaction.state.toString())
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TransactionDetail("Sent", transaction.sent.toString())
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TransactionDetail("Received", transaction.received.toString())
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TransactionDetail("Payments", transaction.payments.toString())
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TransactionDetail("Time lock", transaction.timeLock.toString())
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TransactionDetail("Change", MoneroCurrency.format(transaction.change))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TransactionDetail("Fee", MoneroCurrency.format(transaction.fee))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TransactionDetail("Transaction ID", transaction.txId)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TransactionDetail(
|
||||
label: String,
|
||||
value: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column {
|
||||
Text(label, style = MaterialTheme.typography.labelMedium, modifier = modifier)
|
||||
Text(value, style = MaterialTheme.typography.bodyMedium, modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TransactionCardExpandedPreview(
|
||||
@PreviewParameter(
|
||||
TransactionCardPreviewParameterProvider::class,
|
||||
limit = 1,
|
||||
) transactions: List<Transaction>,
|
||||
) {
|
||||
AppTheme {
|
||||
Surface {
|
||||
TransactionCardExpanded(
|
||||
transaction = transactions.first(),
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TransactionCardPreviewParameterProvider : PreviewParameterProvider<List<Transaction>> {
|
||||
override val values = sequenceOf(PreviewParameterData.transactions)
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package im.molly.monero.demo.ui
|
||||
|
||||
import TransactionCardExpanded
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.ui.Modifier
|
||||
import im.molly.monero.demo.data.model.WalletTransaction
|
||||
|
||||
fun LazyListScope.transactionCardItems(
|
||||
items: List<WalletTransaction>,
|
||||
onTransactionClick: (txId: String, walletId: Long) -> Unit,
|
||||
itemModifier: Modifier = Modifier,
|
||||
) = items(
|
||||
items = items,
|
||||
key = { it.transaction.txId },
|
||||
itemContent = {
|
||||
TransactionCardExpanded(
|
||||
transaction = it.transaction,
|
||||
onClick = { onTransactionClick(it.transaction.txId, it.walletId) },
|
||||
modifier = itemModifier,
|
||||
)
|
||||
},
|
||||
)
|
@ -0,0 +1,158 @@
|
||||
package im.molly.monero.demo.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
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.MoneroCurrency
|
||||
import im.molly.monero.Transaction
|
||||
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
|
||||
|
||||
|
||||
@Composable
|
||||
fun TransactionRoute(
|
||||
txId: String,
|
||||
walletId: Long,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: TransactionViewModel = viewModel(
|
||||
factory = TransactionViewModel.factory(txId, walletId),
|
||||
key = TransactionViewModel.key(txId, walletId),
|
||||
)
|
||||
) {
|
||||
val uiState: TxUiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
TransactionScreen(
|
||||
uiState = uiState,
|
||||
onBackClick = onBackClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TransactionScreen(
|
||||
uiState: TxUiState,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackClick: () -> Unit,
|
||||
) {
|
||||
Scaffold(topBar = {
|
||||
Toolbar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
)
|
||||
}
|
||||
},
|
||||
title = "Transaction details",
|
||||
)
|
||||
}) { padding ->
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
when (uiState) {
|
||||
is TxUiState.Loaded -> {
|
||||
val tx = uiState.transaction
|
||||
TransactionDetail("State", tx.state.toString())
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TransactionDetail("Sent", tx.sent.toString())
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TransactionDetail("Received", tx.received.toString())
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TransactionDetail("Payments", tx.payments.toString())
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TransactionDetail("Time lock", tx.timeLock.toString())
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TransactionDetail("Change", MoneroCurrency.format(tx.change))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TransactionDetail("Fee", MoneroCurrency.format(tx.fee))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TransactionDetail("Transaction ID", tx.txId)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TransactionDetail(
|
||||
label: String,
|
||||
value: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column() {
|
||||
Text(label, style = MaterialTheme.typography.labelMedium, modifier = modifier)
|
||||
Text(value, style = MaterialTheme.typography.bodyMedium, modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TransactionScreenPopulated(
|
||||
@PreviewParameter(
|
||||
TransactionScreenPreviewParameterProvider::class,
|
||||
limit = 1,
|
||||
) transactions: List<Transaction>,
|
||||
) {
|
||||
AppTheme {
|
||||
TransactionScreen(
|
||||
uiState = TxUiState.Loaded(transactions.first()),
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TransactionScreenError() {
|
||||
AppTheme {
|
||||
TransactionScreen(
|
||||
uiState = TxUiState.Error,
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TransactionScreenNotFound() {
|
||||
AppTheme {
|
||||
TransactionScreen(
|
||||
uiState = TxUiState.NotFound,
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class TransactionScreenPreviewParameterProvider : PreviewParameterProvider<List<Transaction>> {
|
||||
override val values = sequenceOf(PreviewParameterData.transactions)
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
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.Transaction
|
||||
import im.molly.monero.demo.AppModule
|
||||
import im.molly.monero.demo.common.Result
|
||||
import im.molly.monero.demo.common.asResult
|
||||
import im.molly.monero.demo.data.WalletRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class TransactionViewModel(
|
||||
txId: String,
|
||||
walletId: Long,
|
||||
walletRepository: WalletRepository = AppModule.walletRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
val uiState: StateFlow<TxUiState> =
|
||||
walletRepository.getTransaction(walletId, txId)
|
||||
.asResult()
|
||||
.map { result ->
|
||||
when (result) {
|
||||
is Result.Success -> result.data?.let { tx ->
|
||||
TxUiState.Loaded(tx)
|
||||
} ?: TxUiState.NotFound
|
||||
|
||||
is Result.Error -> TxUiState.Error
|
||||
is Result.Loading -> TxUiState.Loading
|
||||
}
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = TxUiState.Loading
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun factory(txId: String, walletId: Long) = viewModelFactory {
|
||||
initializer {
|
||||
TransactionViewModel(txId, walletId)
|
||||
}
|
||||
}
|
||||
|
||||
fun key(txId: String, walletId: Long): String = "tx_$txId:$walletId"
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface TxUiState {
|
||||
data class Loaded(val transaction: Transaction) : TxUiState
|
||||
data object Error : TxUiState
|
||||
data object Loading : TxUiState
|
||||
data object NotFound : TxUiState
|
||||
}
|
@ -3,7 +3,9 @@ package im.molly.monero.demo.ui
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.MaterialTheme
|
||||
@ -51,10 +53,12 @@ fun WalletBalanceView(
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
text = "Balance at Block #${blockchainTime.height}",
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
BalanceRow("Confirmed", balance.confirmedAmount)
|
||||
BalanceRow("Pending", balance.pendingAmount)
|
||||
Divider()
|
||||
@ -71,7 +75,6 @@ fun WalletBalanceView(
|
||||
fun BalanceRow(
|
||||
label: String,
|
||||
amount: MoneroAmount,
|
||||
format: MoneroCurrency.Format = MoneroCurrency.Format(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@ -79,8 +82,8 @@ fun BalanceRow(
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(text = label)
|
||||
Text(text = format.format(amount))
|
||||
Text(text = label, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(text = MoneroCurrency.format(amount), style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,7 +93,6 @@ fun LockedBalanceRow(
|
||||
amount: MoneroAmount,
|
||||
blockCount: Int,
|
||||
timeRemaining: Duration,
|
||||
moneroFormat: MoneroCurrency.Format = MoneroCurrency.DefaultFormat,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@ -101,10 +103,10 @@ fun LockedBalanceRow(
|
||||
val durationText =
|
||||
"${timeRemaining.toKotlinDuration()} ($blockCount blocks)"
|
||||
Column {
|
||||
Text(text = label)
|
||||
Text(text = durationText, fontSize = 12.sp)
|
||||
Text(text = label, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(text = durationText, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
Text(text = moneroFormat.format(amount))
|
||||
Text(text = MoneroCurrency.format(amount), style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,50 +5,81 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import im.molly.monero.Balance
|
||||
import im.molly.monero.Ledger
|
||||
import im.molly.monero.demo.data.model.WalletConfig
|
||||
import im.molly.monero.demo.ui.theme.AppTheme
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun WalletCard(
|
||||
walletId: Long,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (Long) -> Unit,
|
||||
viewModel: WalletViewModel = viewModel(
|
||||
factory = WalletViewModel.factory(walletId),
|
||||
key = walletId.toString(),
|
||||
key = WalletViewModel.key(walletId),
|
||||
),
|
||||
) {
|
||||
val walletUiState: WalletUiState by viewModel.walletUiState.collectAsStateWithLifecycle()
|
||||
val uiState: WalletUiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
Card(
|
||||
onClick = { onClick(walletId) },
|
||||
modifier = modifier
|
||||
.padding(8.dp)
|
||||
onClick = onClick, modifier = modifier.padding(8.dp)
|
||||
) {
|
||||
Column {
|
||||
when (walletUiState) {
|
||||
WalletUiState.Error -> {
|
||||
Text(text = "Error") // TODO
|
||||
}
|
||||
WalletUiState.Loading -> {
|
||||
Text(text = "Loading wallet...") // TODO
|
||||
}
|
||||
is WalletUiState.Success -> {
|
||||
val state = walletUiState as WalletUiState.Success
|
||||
Row {
|
||||
Text(text = "Name: ${state.config.name}")
|
||||
}
|
||||
Row {
|
||||
Text(text = "Ledger: ${state.ledger}")
|
||||
}
|
||||
when (uiState) {
|
||||
is WalletUiState.Loaded -> {
|
||||
WalletCardExpanded(
|
||||
(uiState as WalletUiState.Loaded).config,
|
||||
(uiState as WalletUiState.Loaded).balance,
|
||||
)
|
||||
}
|
||||
|
||||
WalletUiState.Error -> WalletCardError()
|
||||
WalletUiState.Loading -> WalletCardLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WalletCardExpanded(
|
||||
config: WalletConfig,
|
||||
balance: Balance,
|
||||
) {
|
||||
Row {
|
||||
Text(text = "Name: ${config.name}")
|
||||
}
|
||||
Row {
|
||||
Text(text = "Ledger: $balance")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WalletCardError() {
|
||||
Text(text = "Error") // TODO
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WalletCardLoading() {
|
||||
Text(text = "Loading wallet...") // TODO
|
||||
}
|
||||
|
||||
//@Preview
|
||||
//@Composable
|
||||
//private fun WalletCardExpandedPreview() {
|
||||
// AppTheme {
|
||||
// Surface {
|
||||
// WalletCardExpanded()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
@ -4,9 +4,9 @@ import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
fun LazyListScope.walletCardsItems(
|
||||
fun LazyListScope.walletCardItems(
|
||||
items: List<Long>,
|
||||
onItemClick: (Long) -> Unit,
|
||||
onItemClick: (walletId: Long) -> Unit,
|
||||
itemModifier: Modifier = Modifier,
|
||||
) = items(
|
||||
items = items,
|
||||
@ -14,7 +14,7 @@ fun LazyListScope.walletCardsItems(
|
||||
itemContent = {
|
||||
WalletCard(
|
||||
walletId = it,
|
||||
onClick = onItemClick,
|
||||
onClick = { onItemClick(it) },
|
||||
modifier = itemModifier,
|
||||
)
|
||||
},
|
||||
|
@ -0,0 +1,31 @@
|
||||
package im.molly.monero.demo.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import im.molly.monero.demo.AppModule
|
||||
import im.molly.monero.demo.data.WalletRepository
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
class WalletListViewModel(
|
||||
walletRepository: WalletRepository = AppModule.walletRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
val uiState: StateFlow<WalletListUiState> =
|
||||
walletRepository.getWalletIdList().map { walletIds ->
|
||||
if (walletIds.isNotEmpty()) {
|
||||
WalletListUiState.Loaded(walletIds)
|
||||
} else {
|
||||
WalletListUiState.Empty
|
||||
}
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = WalletListUiState.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface WalletListUiState {
|
||||
data class Loaded(val walletIds: List<Long>) : WalletListUiState
|
||||
data object Loading : WalletListUiState
|
||||
data object Empty : WalletListUiState
|
||||
}
|
@ -1,39 +1,46 @@
|
||||
package im.molly.monero.demo.ui
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import im.molly.monero.BlockchainTime
|
||||
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.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
|
||||
|
||||
@Composable
|
||||
fun WalletRoute(
|
||||
walletId: Long,
|
||||
onTransactionClick: (String, Long) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: WalletViewModel = viewModel(
|
||||
factory = WalletViewModel.factory(walletId),
|
||||
key = walletId.toString(),
|
||||
key = WalletViewModel.key(walletId),
|
||||
)
|
||||
) {
|
||||
val walletUiState: WalletUiState by viewModel.walletUiState.collectAsStateWithLifecycle()
|
||||
val uiState: WalletUiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
WalletScreen(
|
||||
uiState = walletUiState,
|
||||
uiState = uiState,
|
||||
onWalletConfigChange = { config -> viewModel.updateConfig(config) },
|
||||
onTransactionClick = onTransactionClick,
|
||||
onBackClick = onBackClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
@ -42,20 +49,113 @@ fun WalletRoute(
|
||||
@Composable
|
||||
private fun WalletScreen(
|
||||
uiState: WalletUiState,
|
||||
onWalletConfigChange: (WalletConfig) -> Unit,
|
||||
onTransactionClick: (String, Long) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onWalletConfigChange: (WalletConfig) -> Unit = {},
|
||||
onBackClick: () -> Unit = {},
|
||||
) {
|
||||
when (uiState) {
|
||||
WalletUiState.Error -> WalletScreenError(onBackClick = onBackClick)
|
||||
WalletUiState.Loading -> WalletScreenLoading(onBackClick = onBackClick)
|
||||
is WalletUiState.Success -> WalletScreenPopulated(
|
||||
walletConfig = uiState.config,
|
||||
ledger = uiState.ledger,
|
||||
is WalletUiState.Loaded -> WalletScreenLoaded(
|
||||
uiState = uiState,
|
||||
onWalletConfigChange = onWalletConfigChange,
|
||||
onTransactionClick = onTransactionClick,
|
||||
onBackClick = onBackClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
WalletUiState.Error -> WalletScreenError(onBackClick = onBackClick)
|
||||
WalletUiState.Loading -> WalletScreenLoading(onBackClick = onBackClick)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun WalletScreenLoaded(
|
||||
uiState: WalletUiState.Loaded,
|
||||
onWalletConfigChange: (WalletConfig) -> Unit,
|
||||
onTransactionClick: (String, Long) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showRenameDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(topBar = {
|
||||
Toolbar(navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
)
|
||||
}
|
||||
}, actions = {
|
||||
WalletKebabMenu(
|
||||
onRenameClick = { showRenameDialog = true },
|
||||
onDeleteClick = { },
|
||||
)
|
||||
})
|
||||
}) { padding ->
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(style = MaterialTheme.typography.headlineLarge, text = buildAnnotatedString {
|
||||
append(MoneroCurrency.SYMBOL + " ")
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(
|
||||
MoneroCurrency.Format(precision = 5).format(uiState.balance.confirmedAmount)
|
||||
)
|
||||
}
|
||||
})
|
||||
Text(text = uiState.config.name, style = MaterialTheme.typography.headlineSmall)
|
||||
|
||||
var selectedTabIndex by rememberSaveable { mutableStateOf(0) }
|
||||
|
||||
WalletHeaderTabs(
|
||||
titles = listOf("Balance", "Transactions"),
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
onTabSelected = { index -> selectedTabIndex = index },
|
||||
)
|
||||
|
||||
when (selectedTabIndex) {
|
||||
0 -> {
|
||||
WalletBalanceView(balance = uiState.balance, blockchainTime = uiState.blockchainTime)
|
||||
}
|
||||
|
||||
1 -> {
|
||||
LazyColumn {
|
||||
transactionCardItems(
|
||||
items = uiState.transactions,
|
||||
onTransactionClick = onTransactionClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showRenameDialog) {
|
||||
var name by remember { mutableStateOf(uiState.config.name) }
|
||||
AlertDialog(
|
||||
onDismissRequest = { showRenameDialog = false },
|
||||
title = { Text("Enter wallet name") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
singleLine = true,
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onWalletConfigChange(uiState.config.copy(name = name))
|
||||
showRenameDialog = false
|
||||
}) {
|
||||
Text("Rename")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,85 +171,6 @@ private fun WalletScreenLoading(
|
||||
) {
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun WalletScreenPopulated(
|
||||
walletConfig: WalletConfig,
|
||||
ledger: Ledger,
|
||||
onWalletConfigChange: (WalletConfig) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showRenameDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Toolbar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
WalletKebabMenu(
|
||||
onRenameClick = { showRenameDialog = true },
|
||||
onDeleteClick = { },
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
text = buildAnnotatedString {
|
||||
append(MoneroCurrency.SYMBOL + " ")
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(MoneroCurrency.Format(precision = 5).format(ledger.balance.confirmedAmount))
|
||||
}
|
||||
}
|
||||
)
|
||||
Text(text = walletConfig.name, style = MaterialTheme.typography.headlineSmall)
|
||||
|
||||
WalletBalanceView(
|
||||
balance = ledger.balance,
|
||||
blockchainTime = ledger.checkedAt,
|
||||
)
|
||||
}
|
||||
|
||||
if (showRenameDialog) {
|
||||
var name by remember { mutableStateOf(walletConfig.name) }
|
||||
AlertDialog(
|
||||
onDismissRequest = { showRenameDialog = false },
|
||||
title = { Text("Enter wallet name") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
singleLine = true,
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onWalletConfigChange(walletConfig.copy(name = name))
|
||||
showRenameDialog = false
|
||||
}) {
|
||||
Text("Rename")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WalletKebabMenu(
|
||||
onRenameClick: () -> Unit,
|
||||
@ -184,26 +205,57 @@ private fun WalletKebabMenu(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WalletHeaderTabs(
|
||||
titles: List<String>,
|
||||
selectedTabIndex: Int,
|
||||
onTabSelected: (Int) -> Unit,
|
||||
) {
|
||||
Column {
|
||||
TabRow(selectedTabIndex = selectedTabIndex) {
|
||||
titles.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = index == selectedTabIndex,
|
||||
onClick = { onTabSelected(index) },
|
||||
text = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun WalletScreenPreview() {
|
||||
private fun WalletScreenPopulated(
|
||||
@PreviewParameter(WalletScreenPreviewParameterProvider::class) ledger: Ledger,
|
||||
) {
|
||||
AppTheme {
|
||||
WalletScreen(
|
||||
uiState = WalletUiState.Success(
|
||||
WalletConfig(
|
||||
uiState = WalletUiState.Loaded(
|
||||
config = WalletConfig(
|
||||
id = 0,
|
||||
publicAddress = "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H",
|
||||
publicAddress = ledger.primaryAddress,
|
||||
filename = "",
|
||||
name = "Personal",
|
||||
remoteNodes = emptySet(),
|
||||
),
|
||||
Ledger(
|
||||
publicAddress = "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H",
|
||||
transactions = emptyMap(),
|
||||
enotes = emptySet(),
|
||||
checkedAt = BlockchainTime.Genesis,
|
||||
),
|
||||
balance = ledger.balance,
|
||||
blockchainTime = ledger.checkedAt,
|
||||
transactions = emptyList(),
|
||||
),
|
||||
onWalletConfigChange = {},
|
||||
onTransactionClick = { _: String, _: Long -> },
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class WalletScreenPreviewParameterProvider : PreviewParameterProvider<Ledger> {
|
||||
override val values = sequenceOf(PreviewParameterData.ledger)
|
||||
}
|
||||
|
@ -4,21 +4,23 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import im.molly.monero.Ledger
|
||||
import im.molly.monero.Balance
|
||||
import im.molly.monero.BlockchainTime
|
||||
import im.molly.monero.demo.AppModule
|
||||
import im.molly.monero.demo.common.Result
|
||||
import im.molly.monero.demo.common.asResult
|
||||
import im.molly.monero.demo.data.WalletRepository
|
||||
import im.molly.monero.demo.data.model.WalletConfig
|
||||
import im.molly.monero.demo.data.model.WalletTransaction
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class WalletViewModel(
|
||||
private val walletId: Long,
|
||||
walletId: Long,
|
||||
private val walletRepository: WalletRepository = AppModule.walletRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
val walletUiState: StateFlow<WalletUiState> = walletUiState(
|
||||
val uiState: StateFlow<WalletUiState> = walletUiState(
|
||||
walletId = walletId,
|
||||
walletRepository = walletRepository,
|
||||
).stateIn(
|
||||
@ -33,6 +35,8 @@ class WalletViewModel(
|
||||
WalletViewModel(walletId)
|
||||
}
|
||||
}
|
||||
|
||||
fun key(walletId: Long): String = "wallet_$walletId"
|
||||
}
|
||||
|
||||
fun updateConfig(config: WalletConfig) {
|
||||
@ -53,11 +57,19 @@ private fun walletUiState(
|
||||
).asResult().map { result ->
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
WalletUiState.Success(result.data.first, result.data.second)
|
||||
val config = result.data.first
|
||||
val ledger = result.data.second
|
||||
val balance = ledger.balance
|
||||
val blockchainTime = ledger.checkedAt
|
||||
val transactions =
|
||||
ledger.transactions.map { WalletTransaction(config.id, it.value) }
|
||||
WalletUiState.Loaded(config, blockchainTime, balance, transactions)
|
||||
}
|
||||
|
||||
is Result.Loading -> {
|
||||
WalletUiState.Loading
|
||||
}
|
||||
|
||||
is Result.Error -> {
|
||||
WalletUiState.Error
|
||||
}
|
||||
@ -66,9 +78,11 @@ private fun walletUiState(
|
||||
}
|
||||
|
||||
sealed interface WalletUiState {
|
||||
data class Success(
|
||||
data class Loaded(
|
||||
val config: WalletConfig,
|
||||
val ledger: Ledger,
|
||||
val blockchainTime: BlockchainTime,
|
||||
val balance: Balance,
|
||||
val transactions: List<WalletTransaction>,
|
||||
) : WalletUiState
|
||||
|
||||
data object Error : WalletUiState
|
||||
|
@ -12,8 +12,12 @@ fun NavController.navigateToHistory(navOptions: NavOptions? = null) {
|
||||
navigate(historyNavRoute, navOptions)
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.historyScreen() {
|
||||
fun NavGraphBuilder.historyScreen(
|
||||
navigateToTransaction: (String, Long) -> Unit,
|
||||
) {
|
||||
composable(route = historyNavRoute) {
|
||||
HistoryRoute()
|
||||
HistoryRoute(
|
||||
navigateToTransaction = navigateToTransaction,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -25,13 +25,23 @@ fun NavGraph(
|
||||
navController.navigateToAddWalletWizardGraph()
|
||||
},
|
||||
)
|
||||
historyScreen()
|
||||
historyScreen(
|
||||
navigateToTransaction = { txId, walletId ->
|
||||
navController.navigateToTransaction(txId, walletId)
|
||||
},
|
||||
)
|
||||
settingsScreen(
|
||||
navigateToEditRemoteNode = { remoteNodeId ->
|
||||
navController.navigateToEditRemoteNode(remoteNodeId)
|
||||
},
|
||||
)
|
||||
walletScreen(
|
||||
navigateToTransaction = { txId, walletId ->
|
||||
navController.navigateToTransaction(txId, walletId)
|
||||
},
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
transactionScreen(
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
editRemoteNodeDialog(
|
||||
|
@ -0,0 +1,36 @@
|
||||
package im.molly.monero.demo.ui.navigation
|
||||
|
||||
import androidx.navigation.*
|
||||
import androidx.navigation.compose.composable
|
||||
import im.molly.monero.demo.ui.TransactionRoute
|
||||
|
||||
const val transactionNavRoute = "tx"
|
||||
|
||||
private const val txIdArg = "txId"
|
||||
private const val walletIdArg = "walletId"
|
||||
|
||||
fun NavController.navigateToTransaction(txId: String, walletId: Long) {
|
||||
val route = "$transactionNavRoute/$txId/$walletId"
|
||||
navigate(route)
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.transactionScreen(
|
||||
onBackClick: () -> Unit,
|
||||
) {
|
||||
composable(
|
||||
route = "$transactionNavRoute/{$txIdArg}/{$walletIdArg}",
|
||||
arguments = listOf(
|
||||
navArgument(txIdArg) { type = NavType.StringType },
|
||||
navArgument(walletIdArg) { type = NavType.LongType },
|
||||
)
|
||||
) {
|
||||
val arguments = requireNotNull(it.arguments)
|
||||
val txId = arguments.getString(txIdArg)
|
||||
val walletId = arguments.getLong(walletIdArg)
|
||||
TransactionRoute(
|
||||
txId = txId!!,
|
||||
walletId = walletId,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
}
|
@ -32,6 +32,7 @@ fun NavController.navigateToAddWalletSecondStep(restoreWallet: Boolean) {
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.walletScreen(
|
||||
navigateToTransaction: (String, Long) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
) {
|
||||
composable(
|
||||
@ -44,6 +45,7 @@ fun NavGraphBuilder.walletScreen(
|
||||
val walletId = arguments.getLong(walletIdArg)
|
||||
WalletRoute(
|
||||
walletId = walletId,
|
||||
onTransactionClick = navigateToTransaction,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
package im.molly.monero.demo.ui.preview
|
||||
|
||||
import im.molly.monero.BlockHeader
|
||||
import im.molly.monero.BlockchainTime
|
||||
import im.molly.monero.HashDigest
|
||||
import im.molly.monero.Ledger
|
||||
import im.molly.monero.MoneroAmount
|
||||
import im.molly.monero.PaymentDetail
|
||||
import im.molly.monero.PublicAddress
|
||||
import im.molly.monero.Transaction
|
||||
import im.molly.monero.TxState
|
||||
import im.molly.monero.xmr
|
||||
|
||||
object PreviewParameterData {
|
||||
val recipients =
|
||||
listOf(PublicAddress.parse("888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H"))
|
||||
|
||||
val transactions = listOf(
|
||||
Transaction(
|
||||
hash = HashDigest("e7a60483591378d536792d070f2bf6ccb7d0666df03b57f485ddaf66899a294b"),
|
||||
state = TxState.OnChain(BlockHeader(height = 2999840, epochSecond = 1697792826)),
|
||||
timeLock = BlockchainTime.Block(2999850),
|
||||
sent = emptySet(),
|
||||
received = emptySet(),
|
||||
payments = listOf(PaymentDetail((0.10).xmr, recipients.first())),
|
||||
fee = 0.00093088.xmr,
|
||||
change = MoneroAmount.ZERO,
|
||||
),
|
||||
)
|
||||
|
||||
val ledger = Ledger(
|
||||
primaryAddress = "4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T",
|
||||
checkedAt = BlockchainTime.Block(2999840),
|
||||
enotes = emptySet(),
|
||||
transactions = transactions.associateBy { it.txId },
|
||||
)
|
||||
}
|
@ -6,14 +6,18 @@ data class AccountAddress(
|
||||
val subAddressIndex: Int = 0,
|
||||
) : PublicAddress by publicAddress {
|
||||
|
||||
val isPrimaryAddress: Boolean
|
||||
get() = accountIndex == 0 && subAddressIndex == 0
|
||||
|
||||
init {
|
||||
when (publicAddress) {
|
||||
is SubAddress -> require(accountIndex != -1 && subAddressIndex != -1)
|
||||
else -> require(accountIndex == 0 && subAddressIndex == 0)
|
||||
is StandardAddress -> require(isPrimaryAddress) {
|
||||
"Standard addresses must have subaddress indices set to zero"
|
||||
}
|
||||
is SubAddress -> require(accountIndex != -1 && subAddressIndex != -1) {
|
||||
"Invalid subaddress indices"
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unsupported address type")
|
||||
}
|
||||
|
||||
fun belongsTo(targetAccountIndex: Int): Boolean {
|
||||
return accountIndex == targetAccountIndex
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,8 @@ class Enote(
|
||||
val sourceTxId: String?,
|
||||
) {
|
||||
init {
|
||||
require(age >= 0) { "Enote age $age must not be negative" }
|
||||
require(amount > 0) { "Amount must be greater than 0" }
|
||||
require(age >= 0) { "Age cannot be negative" }
|
||||
}
|
||||
|
||||
var spent: Boolean = false
|
||||
|
@ -3,7 +3,7 @@ package im.molly.monero
|
||||
//import im.molly.monero.proto.LedgerProto
|
||||
|
||||
data class Ledger(
|
||||
val publicAddress: String,
|
||||
val primaryAddress: String,
|
||||
val transactions: Map<String, Transaction>,
|
||||
val enotes: Set<TimeLocked<Enote>>,
|
||||
val checkedAt: BlockchainTime,
|
||||
|
@ -15,7 +15,8 @@ value class MoneroAmount(val atomicUnits: Long) : Parcelable {
|
||||
val ZERO = MoneroAmount(0)
|
||||
}
|
||||
|
||||
fun toXmr(): BigDecimal = BigDecimal.valueOf(atomicUnits, ATOMIC_UNIT_SCALE)
|
||||
val xmr: BigDecimal
|
||||
get() = BigDecimal.valueOf(atomicUnits, ATOMIC_UNIT_SCALE)
|
||||
|
||||
override fun toString() = atomicUnits.toString()
|
||||
|
||||
|
@ -8,10 +8,8 @@ object MoneroCurrency {
|
||||
|
||||
const val MAX_PRECISION = MoneroAmount.ATOMIC_UNIT_SCALE
|
||||
|
||||
val DefaultFormat = Format()
|
||||
|
||||
data class Format(
|
||||
val precision: Int = MAX_PRECISION,
|
||||
open class Format(
|
||||
val precision: Int,
|
||||
val locale: Locale = Locale.US,
|
||||
) {
|
||||
init {
|
||||
@ -24,10 +22,36 @@ object MoneroCurrency {
|
||||
minimumFractionDigits = precision
|
||||
}
|
||||
|
||||
fun format(amount: MoneroAmount): String = numberFormat.format(amount.toXmr())
|
||||
open fun format(amount: MoneroAmount): String {
|
||||
return numberFormat.format(amount.xmr)
|
||||
}
|
||||
|
||||
fun parse(source: String): MoneroAmount {
|
||||
open fun parse(source: String): MoneroAmount {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
val ExactFormat = object : Format(MoneroAmount.ATOMIC_UNIT_SCALE) {
|
||||
override fun format(amount: MoneroAmount) = buildString {
|
||||
val num = amount.atomicUnits.toString()
|
||||
|
||||
if (precision < num.length) {
|
||||
val point = num.length - precision
|
||||
append(num.substring(0, point))
|
||||
append('.')
|
||||
append(num.substring(point))
|
||||
} else {
|
||||
append("0.")
|
||||
for (i in 1..(precision - num.length)) {
|
||||
append('0')
|
||||
}
|
||||
append(num)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun format(amount: MoneroAmount, outputFormat: Format = ExactFormat): String {
|
||||
return outputFormat.format(amount)
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,8 @@ package im.molly.monero
|
||||
|
||||
import im.molly.monero.util.decodeBase58
|
||||
|
||||
interface PublicAddress {
|
||||
sealed interface PublicAddress {
|
||||
val address: String
|
||||
val network: MoneroNetwork
|
||||
val subAddress: Boolean
|
||||
// viewPublicKey: ByteArray
|
||||
@ -19,19 +20,18 @@ interface PublicAddress {
|
||||
throw InvalidAddress("Address too short")
|
||||
}
|
||||
|
||||
val prefix = decoded[0].toLong()
|
||||
|
||||
StandardAddress.prefixes[prefix]?.let { network ->
|
||||
return StandardAddress(network)
|
||||
return when (val prefix = decoded[0].toLong()) {
|
||||
in StandardAddress.prefixes -> {
|
||||
StandardAddress(publicAddress, StandardAddress.prefixes[prefix]!!)
|
||||
}
|
||||
SubAddress.prefixes[prefix]?.let { network ->
|
||||
return SubAddress(network)
|
||||
in SubAddress.prefixes -> {
|
||||
SubAddress(publicAddress, SubAddress.prefixes[prefix]!!)
|
||||
}
|
||||
IntegratedAddress.prefixes[prefix]?.let { network ->
|
||||
in IntegratedAddress.prefixes -> {
|
||||
TODO()
|
||||
}
|
||||
|
||||
throw InvalidAddress("Unrecognized address prefix")
|
||||
else -> throw InvalidAddress("Unrecognized address prefix")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -39,6 +39,7 @@ interface PublicAddress {
|
||||
class InvalidAddress(message: String, cause: Throwable? = null) : Exception(message, cause)
|
||||
|
||||
data class StandardAddress(
|
||||
override val address: String,
|
||||
override val network: MoneroNetwork,
|
||||
) : PublicAddress {
|
||||
override val subAddress = false
|
||||
@ -50,9 +51,12 @@ data class StandardAddress(
|
||||
24L to MoneroNetwork.Stagenet,
|
||||
)
|
||||
}
|
||||
|
||||
override fun toString(): String = address
|
||||
}
|
||||
|
||||
data class SubAddress(
|
||||
override val address: String,
|
||||
override val network: MoneroNetwork,
|
||||
) : PublicAddress {
|
||||
override val subAddress = true
|
||||
@ -64,9 +68,12 @@ data class SubAddress(
|
||||
36L to MoneroNetwork.Stagenet,
|
||||
)
|
||||
}
|
||||
|
||||
override fun toString(): String = address
|
||||
}
|
||||
|
||||
data class IntegratedAddress(
|
||||
override val address: String,
|
||||
override val network: MoneroNetwork,
|
||||
val paymentId: Long,
|
||||
) : PublicAddress {
|
||||
@ -79,4 +86,6 @@ data class IntegratedAddress(
|
||||
25L to MoneroNetwork.Stagenet,
|
||||
)
|
||||
}
|
||||
|
||||
override fun toString(): String = address
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user