mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 02:45:20 +00:00
nice
This commit is contained in:
@@ -51,7 +51,8 @@ import java.util.*
|
||||
@Composable
|
||||
fun ChatScreen(
|
||||
viewModel: ChatViewModel,
|
||||
onWalletClick: () -> Unit = {}
|
||||
onWalletClick: () -> Unit = {},
|
||||
onWalletClickWithToken: ((String) -> Unit)? = null
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val messages by viewModel.messages.observeAsState(emptyList())
|
||||
@@ -109,11 +110,9 @@ fun ChatScreen(
|
||||
messages = displayMessages,
|
||||
currentUserNickname = nickname,
|
||||
meshService = viewModel.meshService,
|
||||
onCashuPaymentClick = { token ->
|
||||
onCashuPaymentClick = { parsedToken ->
|
||||
// Open wallet with the receive dialog pre-filled with this token
|
||||
onWalletClick()
|
||||
// TODO: We need to pass the token to the wallet somehow
|
||||
// This could be done through a shared state, intent, or callback
|
||||
onWalletClickWithToken?.invoke(parsedToken.originalString) ?: onWalletClick()
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
@@ -54,7 +54,12 @@ fun MainAppScreen(
|
||||
when (selectedTab) {
|
||||
0 -> ChatScreen(
|
||||
viewModel = chatViewModel,
|
||||
onWalletClick = { selectedTab = 1 } // Switch to wallet tab when header button is clicked
|
||||
onWalletClick = { selectedTab = 1 }, // Switch to wallet tab when header button is clicked
|
||||
onWalletClickWithToken = { token ->
|
||||
// Switch to wallet and open receive dialog with the token
|
||||
selectedTab = 1
|
||||
walletViewModel.openReceiveDialogWithToken(token)
|
||||
}
|
||||
)
|
||||
1 -> WalletScreen(
|
||||
walletViewModel = walletViewModel,
|
||||
|
||||
@@ -174,7 +174,7 @@ private fun CashuReceiveContent(
|
||||
decodedToken: CashuToken?,
|
||||
isLoading: Boolean
|
||||
) {
|
||||
var token by remember { mutableStateOf("") }
|
||||
val token by viewModel.tokenInput.observeAsState("")
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
if (decodedToken != null) {
|
||||
@@ -302,10 +302,7 @@ private fun CashuReceiveContent(
|
||||
OutlinedTextField(
|
||||
value = token,
|
||||
onValueChange = {
|
||||
token = it
|
||||
if (it.isNotEmpty() && it.startsWith("cashu")) {
|
||||
viewModel.decodeCashuToken(it)
|
||||
}
|
||||
viewModel.setTokenInput(it)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
@@ -354,8 +351,7 @@ private fun CashuReceiveContent(
|
||||
onClick = {
|
||||
clipboardManager.getText()?.text?.let { clipText ->
|
||||
if (clipText.startsWith("cashu")) {
|
||||
token = clipText
|
||||
viewModel.decodeCashuToken(clipText)
|
||||
viewModel.setTokenInput(clipText)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
package com.bitchat.android.wallet.ui
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.bitchat.android.wallet.viewmodel.WalletViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* Fullscreen success animation component for wallet operations
|
||||
*/
|
||||
@Composable
|
||||
fun SuccessAnimation(
|
||||
animationData: WalletViewModel.SuccessAnimationData,
|
||||
onAnimationComplete: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var isVisible by remember { mutableStateOf(true) }
|
||||
|
||||
// Auto-hide after animation duration
|
||||
LaunchedEffect(animationData) {
|
||||
delay(2500) // Show for 2.5 seconds
|
||||
isVisible = false
|
||||
delay(300) // Allow fade out animation to complete
|
||||
onAnimationComplete()
|
||||
}
|
||||
|
||||
// Scale animation for the icon
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "success_animation")
|
||||
val iconScale by infiniteTransition.animateFloat(
|
||||
initialValue = 1f,
|
||||
targetValue = 1.1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 1000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "icon_scale"
|
||||
)
|
||||
|
||||
// Pulsing background effect
|
||||
val backgroundAlpha by infiniteTransition.animateFloat(
|
||||
initialValue = 0.9f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 1500, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "background_alpha"
|
||||
)
|
||||
|
||||
// Entry animation
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (isVisible) 1f else 0.8f,
|
||||
animationSpec = spring(dampingRatio = 0.6f, stiffness = Spring.StiffnessLow),
|
||||
label = "entry_scale"
|
||||
)
|
||||
|
||||
val alpha by animateFloatAsState(
|
||||
targetValue = if (isVisible) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
label = "entry_alpha"
|
||||
)
|
||||
|
||||
if (alpha > 0f) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = backgroundAlpha * alpha)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
modifier = Modifier.scale(scale)
|
||||
) {
|
||||
// Success icon with pulsing circle background
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.size(120.dp)
|
||||
) {
|
||||
// Pulsing background circle
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(100.dp)
|
||||
.background(
|
||||
Color(0xFF00C851).copy(alpha = 0.2f),
|
||||
CircleShape
|
||||
)
|
||||
)
|
||||
|
||||
// Inner circle
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.background(
|
||||
Color(0xFF00C851),
|
||||
CircleShape
|
||||
)
|
||||
.scale(iconScale),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = getIconForAnimationType(animationData.type),
|
||||
contentDescription = "Success",
|
||||
tint = Color.Black,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Success message
|
||||
Text(
|
||||
text = "SUCCESS!",
|
||||
color = Color(0xFF00C851),
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
letterSpacing = 2.sp
|
||||
)
|
||||
|
||||
// Amount
|
||||
Text(
|
||||
text = formatAmount(animationData.amount, animationData.unit),
|
||||
color = Color.White,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
// Description
|
||||
Text(
|
||||
text = animationData.description,
|
||||
color = Color.Gray,
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 32.dp)
|
||||
)
|
||||
|
||||
// Animated checkmark or icon indicator
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
for (i in 0..2) {
|
||||
val dotAlpha by infiniteTransition.animateFloat(
|
||||
initialValue = 0.3f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 600),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(i * 200)
|
||||
),
|
||||
label = "dot_$i"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(
|
||||
Color(0xFF00C851).copy(alpha = dotAlpha),
|
||||
CircleShape
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get appropriate icon for animation type
|
||||
*/
|
||||
private fun getIconForAnimationType(type: WalletViewModel.SuccessAnimationType): ImageVector {
|
||||
return when (type) {
|
||||
WalletViewModel.SuccessAnimationType.CASHU_RECEIVED -> Icons.Filled.Download
|
||||
WalletViewModel.SuccessAnimationType.CASHU_SENT -> Icons.Filled.Upload
|
||||
WalletViewModel.SuccessAnimationType.LIGHTNING_RECEIVED -> Icons.Filled.FlashOn
|
||||
WalletViewModel.SuccessAnimationType.LIGHTNING_SENT -> Icons.AutoMirrored.Filled.Send
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format amount for display
|
||||
*/
|
||||
private fun formatAmount(amount: Long, unit: String): String {
|
||||
return when (unit.lowercase()) {
|
||||
"sat", "sats" -> {
|
||||
when {
|
||||
amount >= 100_000_000 -> String.format("%.2f BTC", amount / 100_000_000.0)
|
||||
amount >= 1000 -> String.format("%,d sats", amount)
|
||||
else -> "$amount sats"
|
||||
}
|
||||
}
|
||||
else -> "$amount $unit"
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,12 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.bitchat.android.wallet.viewmodel.WalletViewModel
|
||||
|
||||
// Import the SuccessAnimation component (same package, no need for full path)
|
||||
|
||||
/**
|
||||
* Main wallet screen with bottom navigation
|
||||
*/
|
||||
@@ -30,6 +33,8 @@ fun WalletScreen(
|
||||
var showSendView by remember { mutableStateOf(false) }
|
||||
val showSendDialog by walletViewModel.showSendDialog.observeAsState(false)
|
||||
val showReceiveDialog by walletViewModel.showReceiveDialog.observeAsState(false)
|
||||
val showSuccessAnimation by walletViewModel.showSuccessAnimation.observeAsState(false)
|
||||
val successAnimationData by walletViewModel.successAnimationData.observeAsState()
|
||||
|
||||
// Back handler for the wallet
|
||||
fun handleBackPress(): Boolean {
|
||||
@@ -64,74 +69,88 @@ fun WalletScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (showReceiveView) {
|
||||
// Full-screen ReceiveView
|
||||
ReceiveView(
|
||||
viewModel = walletViewModel,
|
||||
onNavigateBack = {
|
||||
showReceiveView = false
|
||||
walletViewModel.hideReceiveDialog()
|
||||
}
|
||||
)
|
||||
} else if (showSendView) {
|
||||
// Full-screen SendView
|
||||
SendView(
|
||||
viewModel = walletViewModel,
|
||||
onNavigateBack = {
|
||||
showSendView = false
|
||||
walletViewModel.hideSendDialog()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Column(modifier = modifier.fillMaxSize()) {
|
||||
// Content
|
||||
when (selectedTab) {
|
||||
0 -> WalletOverview(
|
||||
viewModel = walletViewModel,
|
||||
onBackToChat = onBackToChat,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
1 -> TransactionHistory(
|
||||
transactions = walletViewModel.getAllTransactions().observeAsState(initial = emptyList()).value,
|
||||
modifier = Modifier.weight(1f),
|
||||
onTransactionClick = { transaction ->
|
||||
// For lightning receive transactions, open the receive view with the quote
|
||||
if (transaction.type == com.bitchat.android.wallet.data.TransactionType.LIGHTNING_RECEIVE &&
|
||||
transaction.quote != null) {
|
||||
walletViewModel.setCurrentMintQuote(transaction.quote!!)
|
||||
showReceiveView = true
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
// Main content
|
||||
if (showReceiveView) {
|
||||
// Full-screen ReceiveView
|
||||
ReceiveView(
|
||||
viewModel = walletViewModel,
|
||||
onNavigateBack = {
|
||||
showReceiveView = false
|
||||
walletViewModel.hideReceiveDialog()
|
||||
}
|
||||
)
|
||||
} else if (showSendView) {
|
||||
// Full-screen SendView
|
||||
SendView(
|
||||
viewModel = walletViewModel,
|
||||
onNavigateBack = {
|
||||
showSendView = false
|
||||
walletViewModel.hideSendDialog()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Content
|
||||
when (selectedTab) {
|
||||
0 -> WalletOverview(
|
||||
viewModel = walletViewModel,
|
||||
onBackToChat = onBackToChat,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
1 -> TransactionHistory(
|
||||
transactions = walletViewModel.getAllTransactions().observeAsState(initial = emptyList()).value,
|
||||
modifier = Modifier.weight(1f),
|
||||
onTransactionClick = { transaction ->
|
||||
// For lightning receive transactions, open the receive view with the quote
|
||||
if (transaction.type == com.bitchat.android.wallet.data.TransactionType.LIGHTNING_RECEIVE &&
|
||||
transaction.quote != null) {
|
||||
walletViewModel.setCurrentMintQuote(transaction.quote!!)
|
||||
showReceiveView = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
2 -> MintsScreen(viewModel = walletViewModel, modifier = Modifier.weight(1f))
|
||||
3 -> WalletSettings(
|
||||
viewModel = walletViewModel,
|
||||
onBackClick = { /* No back action needed in tab navigation */ }
|
||||
)
|
||||
2 -> MintsScreen(viewModel = walletViewModel, modifier = Modifier.weight(1f))
|
||||
3 -> WalletSettings(
|
||||
viewModel = walletViewModel,
|
||||
onBackClick = { /* No back action needed in tab navigation */ }
|
||||
)
|
||||
}
|
||||
|
||||
// Bottom Navigation
|
||||
WalletBottomNavigation(
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = { selectedTab = it }
|
||||
)
|
||||
}
|
||||
|
||||
// Bottom Navigation
|
||||
WalletBottomNavigation(
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = { selectedTab = it }
|
||||
}
|
||||
|
||||
// Success animation overlay
|
||||
if (showSuccessAnimation && successAnimationData != null) {
|
||||
SuccessAnimation(
|
||||
animationData = successAnimationData!!,
|
||||
onAnimationComplete = {
|
||||
walletViewModel.hideSuccessAnimation()
|
||||
},
|
||||
modifier = Modifier.zIndex(10f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle dialog states
|
||||
if (showSendDialog) {
|
||||
LaunchedEffect(showSendDialog) {
|
||||
if (showSendDialog) {
|
||||
showSendView = true
|
||||
}
|
||||
// Handle dialog states - sync local view state with ViewModel dialog state
|
||||
LaunchedEffect(showSendDialog) {
|
||||
if (showSendDialog) {
|
||||
showSendView = true
|
||||
} else {
|
||||
showSendView = false
|
||||
}
|
||||
}
|
||||
|
||||
if (showReceiveDialog) {
|
||||
LaunchedEffect(showReceiveDialog) {
|
||||
if (showReceiveDialog) {
|
||||
showReceiveView = true
|
||||
}
|
||||
LaunchedEffect(showReceiveDialog) {
|
||||
if (showReceiveDialog) {
|
||||
showReceiveView = true
|
||||
} else {
|
||||
showReceiveView = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,12 +82,40 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||
private val _currentMeltQuote = MutableLiveData<MeltQuote?>(null)
|
||||
val currentMeltQuote: LiveData<MeltQuote?> = _currentMeltQuote
|
||||
|
||||
// Token input state for receive dialog
|
||||
private val _tokenInput = MutableLiveData<String>("")
|
||||
val tokenInput: LiveData<String> = _tokenInput
|
||||
|
||||
// Success animation state
|
||||
private val _showSuccessAnimation = MutableLiveData<Boolean>(false)
|
||||
val showSuccessAnimation: LiveData<Boolean> = _showSuccessAnimation
|
||||
|
||||
private val _successAnimationData = MutableLiveData<SuccessAnimationData?>(null)
|
||||
val successAnimationData: LiveData<SuccessAnimationData?> = _successAnimationData
|
||||
|
||||
// State management
|
||||
private var pollingJob: kotlinx.coroutines.Job? = null
|
||||
|
||||
enum class SendType { CASHU, LIGHTNING }
|
||||
enum class ReceiveType { CASHU, LIGHTNING }
|
||||
|
||||
/**
|
||||
* Data for success animation display
|
||||
*/
|
||||
data class SuccessAnimationData(
|
||||
val type: SuccessAnimationType,
|
||||
val amount: Long,
|
||||
val unit: String,
|
||||
val description: String
|
||||
)
|
||||
|
||||
enum class SuccessAnimationType {
|
||||
CASHU_RECEIVED,
|
||||
CASHU_SENT,
|
||||
LIGHTNING_RECEIVED,
|
||||
LIGHTNING_SENT
|
||||
}
|
||||
|
||||
init {
|
||||
loadInitialData()
|
||||
startPolling()
|
||||
@@ -359,6 +387,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||
_decodedToken.value = null
|
||||
_currentMintQuote.value = null
|
||||
_receiveType.value = ReceiveType.CASHU
|
||||
clearTokenInput()
|
||||
}
|
||||
|
||||
fun setSendType(type: SendType) {
|
||||
@@ -381,6 +410,30 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||
_errorMessage.value = null
|
||||
}
|
||||
|
||||
// Token input management
|
||||
fun setTokenInput(token: String) {
|
||||
_tokenInput.value = token
|
||||
if (token.isNotEmpty() && token.startsWith("cashu")) {
|
||||
decodeCashuToken(token)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearTokenInput() {
|
||||
_tokenInput.value = ""
|
||||
_decodedToken.value = null
|
||||
}
|
||||
|
||||
// Success animation management
|
||||
fun showSuccessAnimation(animationData: SuccessAnimationData) {
|
||||
_successAnimationData.value = animationData
|
||||
_showSuccessAnimation.value = true
|
||||
}
|
||||
|
||||
fun hideSuccessAnimation() {
|
||||
_showSuccessAnimation.value = false
|
||||
_successAnimationData.value = null
|
||||
}
|
||||
|
||||
// Back navigation handler
|
||||
private var backHandler: (() -> Boolean)? = null
|
||||
|
||||
@@ -488,7 +541,23 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||
repository.saveTransaction(transaction).onSuccess {
|
||||
loadTransactions()
|
||||
refreshBalance()
|
||||
|
||||
// Clear token input immediately
|
||||
clearTokenInput()
|
||||
|
||||
// Show success animation
|
||||
val animationData = SuccessAnimationData(
|
||||
type = SuccessAnimationType.CASHU_RECEIVED,
|
||||
amount = amount,
|
||||
unit = "sat",
|
||||
description = "Cashu token received successfully!"
|
||||
)
|
||||
showSuccessAnimation(animationData)
|
||||
|
||||
// Close receive dialog after animation starts
|
||||
delay(500) // Short delay to let animation start
|
||||
hideReceiveDialog()
|
||||
|
||||
}.onFailure { error ->
|
||||
Log.e(TAG, "Failed to save transaction", error)
|
||||
_errorMessage.value = "Failed to save transaction: ${error.message}"
|
||||
@@ -797,6 +866,47 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||
}
|
||||
}
|
||||
|
||||
// Utility function to format sats
|
||||
private fun formatSats(sats: Long): String {
|
||||
return when {
|
||||
sats >= 100_000_000 -> String.format("%.2f BTC", sats / 100_000_000.0)
|
||||
sats >= 1000 -> String.format("%,d sats", sats)
|
||||
else -> "$sats sats"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open receive dialog with a pre-filled Cashu token (for external integrations like chat)
|
||||
*/
|
||||
fun openReceiveDialogWithToken(tokenString: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
// Clear any existing state first
|
||||
hideReceiveDialog()
|
||||
_tokenInput.value = ""
|
||||
_decodedToken.value = null
|
||||
|
||||
// Set the receive type to Cashu
|
||||
_receiveType.value = ReceiveType.CASHU
|
||||
|
||||
// Set the token input which will trigger decoding
|
||||
_tokenInput.value = tokenString
|
||||
|
||||
// Decode the token
|
||||
decodeCashuToken(tokenString)
|
||||
|
||||
// Show the receive dialog
|
||||
_showReceiveDialog.value = true
|
||||
|
||||
Log.d(TAG, "Opened receive dialog with token: ${tokenString.take(20)}...")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error opening receive dialog with token", e)
|
||||
_errorMessage.value = "Failed to process token: ${e.message}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full transaction history (not just last 10)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user