|
@@ -1,339 +0,0 @@
|
|
|
-package com.codeskraps.publicpool.presentation.wallet
|
|
|
-
|
|
|
-import android.content.ClipData
|
|
|
-import android.content.ClipboardManager
|
|
|
-import android.content.Context
|
|
|
-import android.os.Parcelable
|
|
|
-import android.widget.Toast
|
|
|
-import androidx.compose.foundation.layout.Arrangement
|
|
|
-import androidx.compose.foundation.layout.Box
|
|
|
-import androidx.compose.foundation.layout.Column
|
|
|
-import androidx.compose.foundation.layout.PaddingValues
|
|
|
-import androidx.compose.foundation.layout.Row
|
|
|
-import androidx.compose.foundation.layout.Spacer
|
|
|
-import androidx.compose.foundation.layout.defaultMinSize
|
|
|
-import androidx.compose.foundation.layout.fillMaxSize
|
|
|
-import androidx.compose.foundation.layout.fillMaxWidth
|
|
|
-import androidx.compose.foundation.layout.height
|
|
|
-import androidx.compose.foundation.layout.padding
|
|
|
-import androidx.compose.foundation.lazy.LazyColumn
|
|
|
-import androidx.compose.foundation.lazy.items
|
|
|
-import androidx.compose.material3.CircularProgressIndicator
|
|
|
-import androidx.compose.material3.ExperimentalMaterial3Api
|
|
|
-import androidx.compose.material3.MaterialTheme
|
|
|
-import androidx.compose.material3.Scaffold
|
|
|
-import androidx.compose.material3.Text
|
|
|
-import androidx.compose.material3.TopAppBar
|
|
|
-import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
|
|
-import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
|
|
-import androidx.compose.runtime.Composable
|
|
|
-import androidx.compose.runtime.LaunchedEffect
|
|
|
-import androidx.compose.runtime.collectAsState
|
|
|
-import androidx.compose.runtime.getValue
|
|
|
-import androidx.compose.runtime.remember
|
|
|
-import androidx.compose.ui.Alignment
|
|
|
-import androidx.compose.ui.Modifier
|
|
|
-import androidx.compose.ui.graphics.Color
|
|
|
-import androidx.compose.ui.platform.LocalContext
|
|
|
-import androidx.compose.ui.res.stringResource
|
|
|
-import androidx.compose.ui.text.font.FontWeight
|
|
|
-import androidx.compose.ui.text.style.TextAlign
|
|
|
-import androidx.compose.ui.text.style.TextOverflow
|
|
|
-import androidx.compose.ui.unit.dp
|
|
|
-import cafe.adriel.voyager.core.screen.Screen
|
|
|
-import cafe.adriel.voyager.koin.koinScreenModel
|
|
|
-import com.codeskraps.publicpool.R
|
|
|
-import com.codeskraps.publicpool.domain.model.CryptoPrice
|
|
|
-import com.codeskraps.publicpool.domain.model.WalletInfo
|
|
|
-import com.codeskraps.publicpool.domain.model.WalletTransaction
|
|
|
-import com.codeskraps.publicpool.presentation.common.AppCard
|
|
|
-import com.codeskraps.publicpool.ui.theme.PositiveGreen
|
|
|
-import kotlinx.coroutines.flow.collectLatest
|
|
|
-import kotlinx.parcelize.Parcelize
|
|
|
-import java.text.NumberFormat
|
|
|
-import java.time.format.DateTimeFormatter
|
|
|
-import java.util.Currency
|
|
|
-import java.util.Locale
|
|
|
-
|
|
|
-@Parcelize
|
|
|
-data object WalletScreen : Screen, Parcelable {
|
|
|
- private fun readResolve(): Any = WalletScreen
|
|
|
-
|
|
|
- @OptIn(ExperimentalMaterial3Api::class)
|
|
|
- @Composable
|
|
|
- override fun Content() {
|
|
|
- val screenModel: WalletScreenModel = koinScreenModel()
|
|
|
- val state by screenModel.state.collectAsState()
|
|
|
- val context = LocalContext.current
|
|
|
-
|
|
|
- // Track page view when screen becomes visible
|
|
|
- LaunchedEffect(Unit) {
|
|
|
- screenModel.onScreenVisible()
|
|
|
- }
|
|
|
-
|
|
|
- LaunchedEffect(key1 = screenModel.effect) {
|
|
|
- screenModel.effect.collectLatest { effect ->
|
|
|
- when (effect) {
|
|
|
- is WalletEffect.ShowError -> {
|
|
|
- Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- Scaffold(
|
|
|
- topBar = { TopAppBar(title = { Text(stringResource(R.string.screen_title_wallet_details)) }) }
|
|
|
- ) { paddingValues ->
|
|
|
- PullToRefreshBox(
|
|
|
- isRefreshing = state.isWalletLoading || state.isLoading,
|
|
|
- onRefresh = { screenModel.handleEvent(WalletEvent.LoadWalletDetails) },
|
|
|
- modifier = Modifier
|
|
|
- .fillMaxSize()
|
|
|
- .padding(
|
|
|
- top = paddingValues.calculateTopPadding(),
|
|
|
- start = 0.dp,
|
|
|
- end = 0.dp,
|
|
|
- bottom = 0.dp
|
|
|
- )
|
|
|
- ) {
|
|
|
- Box(
|
|
|
- modifier = Modifier.fillMaxSize()
|
|
|
- ) {
|
|
|
- when {
|
|
|
- state.isWalletLoading || state.isLoading -> {
|
|
|
- // Combined loading indicator
|
|
|
- CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
|
|
- }
|
|
|
- state.walletAddress.isNullOrBlank() -> {
|
|
|
- // Use stringResource for message
|
|
|
- Text(
|
|
|
- text = stringResource(R.string.wallet_error_not_set),
|
|
|
- modifier = Modifier.align(Alignment.Center).padding(16.dp),
|
|
|
- textAlign = TextAlign.Center
|
|
|
- )
|
|
|
- }
|
|
|
- state.errorMessage != null -> {
|
|
|
- // Use stringResource for generic error, keep specific from state
|
|
|
- Text(
|
|
|
- text = state.errorMessage ?: stringResource(R.string.error_unknown),
|
|
|
- modifier = Modifier.align(Alignment.Center).padding(16.dp),
|
|
|
- color = MaterialTheme.colorScheme.error,
|
|
|
- textAlign = TextAlign.Center
|
|
|
- )
|
|
|
- }
|
|
|
- state.walletInfo == null -> {
|
|
|
- // Use stringResource for message
|
|
|
- Text(
|
|
|
- text = stringResource(R.string.wallet_error_load_failed),
|
|
|
- modifier = Modifier.align(Alignment.Center).padding(16.dp),
|
|
|
- textAlign = TextAlign.Center
|
|
|
- )
|
|
|
- }
|
|
|
- else -> {
|
|
|
- // Display Wallet Info and Transactions
|
|
|
- WalletDetailsContent(
|
|
|
- walletInfo = state.walletInfo!!,
|
|
|
- btcPrice = state.btcPrice
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-@Composable
|
|
|
-fun WalletDetailsContent(
|
|
|
- walletInfo: WalletInfo,
|
|
|
- btcPrice: CryptoPrice?
|
|
|
-) {
|
|
|
- val context = LocalContext.current
|
|
|
- val btcFormat = remember { "%.8f BTC" }
|
|
|
- val dateFormatter = remember { DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") }
|
|
|
- val displayCurrency = stringResource(R.string.currency_usd)
|
|
|
- val currencyFormat = remember {
|
|
|
- NumberFormat.getCurrencyInstance(Locale.US).apply {
|
|
|
- currency = Currency.getInstance(displayCurrency)
|
|
|
- maximumFractionDigits = 2
|
|
|
- minimumFractionDigits = 2
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- val finalBalanceFiat = remember(walletInfo.finalBalanceBtc, btcPrice) {
|
|
|
- btcPrice?.let { walletInfo.finalBalanceBtc * it.price }
|
|
|
- }
|
|
|
-
|
|
|
- LazyColumn(
|
|
|
- modifier = Modifier.fillMaxSize(),
|
|
|
- contentPadding = PaddingValues(16.dp),
|
|
|
- verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
|
- ) {
|
|
|
- // Current Price Section
|
|
|
- item {
|
|
|
- CurrentPriceCard(btcPrice = btcPrice, currencyFormat = currencyFormat)
|
|
|
- }
|
|
|
-
|
|
|
- // Balance Section
|
|
|
- item {
|
|
|
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
|
|
- BalanceCard(
|
|
|
- label = stringResource(R.string.wallet_label_final_balance),
|
|
|
- valueBtc = btcFormat.format(Locale.US, walletInfo.finalBalanceBtc),
|
|
|
- valueFiat = finalBalanceFiat?.let { currencyFormat.format(it) },
|
|
|
- fiatCurrencyLabel = btcPrice?.currency ?: stringResource(R.string.currency_usd),
|
|
|
- modifier = Modifier.weight(1f)
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Totals Section
|
|
|
- item {
|
|
|
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
|
|
- BalanceCard(
|
|
|
- label = stringResource(R.string.wallet_label_total_received),
|
|
|
- valueBtc = btcFormat.format(Locale.US, walletInfo.totalReceivedBtc),
|
|
|
- modifier = Modifier.weight(1f)
|
|
|
- )
|
|
|
- BalanceCard(
|
|
|
- label = stringResource(R.string.wallet_label_total_sent),
|
|
|
- valueBtc = btcFormat.format(Locale.US, walletInfo.totalSentBtc),
|
|
|
- modifier = Modifier.weight(1f)
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Transaction Header
|
|
|
- item {
|
|
|
- Text(
|
|
|
- text = stringResource(R.string.wallet_header_recent_transactions_count, walletInfo.transactionCount),
|
|
|
- style = MaterialTheme.typography.titleMedium,
|
|
|
- modifier = Modifier.padding(top = 8.dp) // Add some space before tx list
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
- // Transaction List
|
|
|
- items(walletInfo.transactions, key = { it.hash }) { tx ->
|
|
|
- TransactionItem(tx = tx, dateFormatter = dateFormatter)
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-@Composable
|
|
|
-fun CurrentPriceCard(btcPrice: CryptoPrice?, currencyFormat: NumberFormat) {
|
|
|
- AppCard(modifier = Modifier.fillMaxWidth()) {
|
|
|
- Column(
|
|
|
- modifier = Modifier
|
|
|
- .fillMaxWidth()
|
|
|
- .defaultMinSize(minHeight = 90.dp)
|
|
|
- .padding(12.dp),
|
|
|
- verticalArrangement = Arrangement.SpaceBetween
|
|
|
- ) {
|
|
|
- Text(
|
|
|
- text = stringResource(R.string.wallet_label_current_btc_price),
|
|
|
- style = MaterialTheme.typography.labelMedium,
|
|
|
- color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
|
- )
|
|
|
-
|
|
|
- Box(modifier = Modifier.align(Alignment.End)) {
|
|
|
- if (btcPrice != null) {
|
|
|
- Text(
|
|
|
- text = currencyFormat.format(btcPrice.price),
|
|
|
- style = MaterialTheme.typography.headlineSmall,
|
|
|
- fontWeight = FontWeight.Bold,
|
|
|
- color = MaterialTheme.colorScheme.onSurface
|
|
|
- )
|
|
|
- } else {
|
|
|
- Text(
|
|
|
- text = stringResource(R.string.text_placeholder_dash),
|
|
|
- style = MaterialTheme.typography.headlineSmall,
|
|
|
- fontWeight = FontWeight.Bold,
|
|
|
- color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-@Composable
|
|
|
-fun BalanceCard(
|
|
|
- label: String,
|
|
|
- valueBtc: String,
|
|
|
- valueFiat: String? = null,
|
|
|
- fiatCurrencyLabel: String? = null,
|
|
|
- modifier: Modifier = Modifier
|
|
|
-) {
|
|
|
- AppCard(modifier = modifier) {
|
|
|
- Column(
|
|
|
- modifier = Modifier
|
|
|
- .fillMaxWidth()
|
|
|
- .defaultMinSize(minHeight = 90.dp)
|
|
|
- .padding(12.dp),
|
|
|
- verticalArrangement = Arrangement.SpaceBetween
|
|
|
- ) {
|
|
|
- Text(
|
|
|
- text = label,
|
|
|
- style = MaterialTheme.typography.labelMedium,
|
|
|
- color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
|
- )
|
|
|
-
|
|
|
- Box(modifier = Modifier.align(Alignment.End)) {
|
|
|
- Column(horizontalAlignment = Alignment.End) {
|
|
|
- Text(
|
|
|
- text = valueBtc,
|
|
|
- style = MaterialTheme.typography.bodyLarge,
|
|
|
- fontWeight = FontWeight.Bold,
|
|
|
- color = MaterialTheme.colorScheme.onSurface
|
|
|
- )
|
|
|
- Text(
|
|
|
- text = valueFiat?.let { "${stringResource(R.string.wallet_balance_fiat_prefix)} $it${fiatCurrencyLabel?.let { c -> " $c" } ?: ""}" } ?: "",
|
|
|
- style = MaterialTheme.typography.bodySmall,
|
|
|
- color = if (valueFiat != null) PositiveGreen else Color.Transparent
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-@Composable
|
|
|
-fun TransactionItem(tx: WalletTransaction, dateFormatter: DateTimeFormatter) {
|
|
|
- val btcFormat = remember { "%.8f BTC" }
|
|
|
- val netValueFormatted = btcFormat.format(Locale.US, tx.resultBtc)
|
|
|
- val timeFormatted = tx.time?.format(dateFormatter) ?: "Pending"
|
|
|
- val valueColor = if (tx.resultSatoshis >= 0) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error // Green for positive, Red for negative
|
|
|
-
|
|
|
- AppCard(modifier = Modifier.fillMaxWidth()) {
|
|
|
- Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
|
|
|
- Row(
|
|
|
- modifier = Modifier.fillMaxWidth(),
|
|
|
- horizontalArrangement = Arrangement.SpaceBetween,
|
|
|
- verticalAlignment = Alignment.CenterVertically
|
|
|
- ) {
|
|
|
- Text(text = timeFormatted, style = MaterialTheme.typography.labelMedium)
|
|
|
- Text(text = netValueFormatted, style = MaterialTheme.typography.bodyMedium, color = valueColor)
|
|
|
- }
|
|
|
- Spacer(modifier = Modifier.height(4.dp))
|
|
|
- Text(
|
|
|
- text = tx.hash,
|
|
|
- style = MaterialTheme.typography.bodySmall,
|
|
|
- maxLines = 1,
|
|
|
- overflow = TextOverflow.Ellipsis,
|
|
|
- color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// Helper function to copy text to clipboard
|
|
|
-fun copyToClipboard(context: Context, text: String) {
|
|
|
- val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
|
- // Use context.getString() for non-composable contexts
|
|
|
- val label = context.getString(R.string.wallet_clipboard_label)
|
|
|
- val message = context.getString(R.string.wallet_toast_address_copied)
|
|
|
-
|
|
|
- val clip = ClipData.newPlainText(label, text)
|
|
|
- clipboard.setPrimaryClip(clip)
|
|
|
- Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
|
|
-}
|