mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 08:25:22 +00:00
nice
This commit is contained in:
@@ -51,7 +51,8 @@ import java.util.*
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChatScreen(
|
fun ChatScreen(
|
||||||
viewModel: ChatViewModel,
|
viewModel: ChatViewModel,
|
||||||
onWalletClick: () -> Unit = {}
|
onWalletClick: () -> Unit = {},
|
||||||
|
onWalletClickWithToken: ((String) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val messages by viewModel.messages.observeAsState(emptyList())
|
val messages by viewModel.messages.observeAsState(emptyList())
|
||||||
@@ -109,11 +110,9 @@ fun ChatScreen(
|
|||||||
messages = displayMessages,
|
messages = displayMessages,
|
||||||
currentUserNickname = nickname,
|
currentUserNickname = nickname,
|
||||||
meshService = viewModel.meshService,
|
meshService = viewModel.meshService,
|
||||||
onCashuPaymentClick = { token ->
|
onCashuPaymentClick = { parsedToken ->
|
||||||
// Open wallet with the receive dialog pre-filled with this token
|
// Open wallet with the receive dialog pre-filled with this token
|
||||||
onWalletClick()
|
onWalletClickWithToken?.invoke(parsedToken.originalString) ?: onWalletClick()
|
||||||
// TODO: We need to pass the token to the wallet somehow
|
|
||||||
// This could be done through a shared state, intent, or callback
|
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,7 +54,12 @@ fun MainAppScreen(
|
|||||||
when (selectedTab) {
|
when (selectedTab) {
|
||||||
0 -> ChatScreen(
|
0 -> ChatScreen(
|
||||||
viewModel = chatViewModel,
|
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(
|
1 -> WalletScreen(
|
||||||
walletViewModel = walletViewModel,
|
walletViewModel = walletViewModel,
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ private fun CashuReceiveContent(
|
|||||||
decodedToken: CashuToken?,
|
decodedToken: CashuToken?,
|
||||||
isLoading: Boolean
|
isLoading: Boolean
|
||||||
) {
|
) {
|
||||||
var token by remember { mutableStateOf("") }
|
val token by viewModel.tokenInput.observeAsState("")
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
if (decodedToken != null) {
|
if (decodedToken != null) {
|
||||||
@@ -302,10 +302,7 @@ private fun CashuReceiveContent(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = token,
|
value = token,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
token = it
|
viewModel.setTokenInput(it)
|
||||||
if (it.isNotEmpty() && it.startsWith("cashu")) {
|
|
||||||
viewModel.decodeCashuToken(it)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
label = {
|
label = {
|
||||||
Text(
|
Text(
|
||||||
@@ -354,8 +351,7 @@ private fun CashuReceiveContent(
|
|||||||
onClick = {
|
onClick = {
|
||||||
clipboardManager.getText()?.text?.let { clipText ->
|
clipboardManager.getText()?.text?.let { clipText ->
|
||||||
if (clipText.startsWith("cashu")) {
|
if (clipText.startsWith("cashu")) {
|
||||||
token = clipText
|
viewModel.setTokenInput(clipText)
|
||||||
viewModel.decodeCashuToken(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.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.bitchat.android.wallet.viewmodel.WalletViewModel
|
import com.bitchat.android.wallet.viewmodel.WalletViewModel
|
||||||
|
|
||||||
|
// Import the SuccessAnimation component (same package, no need for full path)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main wallet screen with bottom navigation
|
* Main wallet screen with bottom navigation
|
||||||
*/
|
*/
|
||||||
@@ -30,6 +33,8 @@ fun WalletScreen(
|
|||||||
var showSendView by remember { mutableStateOf(false) }
|
var showSendView by remember { mutableStateOf(false) }
|
||||||
val showSendDialog by walletViewModel.showSendDialog.observeAsState(false)
|
val showSendDialog by walletViewModel.showSendDialog.observeAsState(false)
|
||||||
val showReceiveDialog by walletViewModel.showReceiveDialog.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
|
// Back handler for the wallet
|
||||||
fun handleBackPress(): Boolean {
|
fun handleBackPress(): Boolean {
|
||||||
@@ -64,74 +69,88 @@ fun WalletScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showReceiveView) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
// Full-screen ReceiveView
|
// Main content
|
||||||
ReceiveView(
|
if (showReceiveView) {
|
||||||
viewModel = walletViewModel,
|
// Full-screen ReceiveView
|
||||||
onNavigateBack = {
|
ReceiveView(
|
||||||
showReceiveView = false
|
viewModel = walletViewModel,
|
||||||
walletViewModel.hideReceiveDialog()
|
onNavigateBack = {
|
||||||
}
|
showReceiveView = false
|
||||||
)
|
walletViewModel.hideReceiveDialog()
|
||||||
} else if (showSendView) {
|
}
|
||||||
// Full-screen SendView
|
)
|
||||||
SendView(
|
} else if (showSendView) {
|
||||||
viewModel = walletViewModel,
|
// Full-screen SendView
|
||||||
onNavigateBack = {
|
SendView(
|
||||||
showSendView = false
|
viewModel = walletViewModel,
|
||||||
walletViewModel.hideSendDialog()
|
onNavigateBack = {
|
||||||
}
|
showSendView = false
|
||||||
)
|
walletViewModel.hideSendDialog()
|
||||||
} else {
|
}
|
||||||
Column(modifier = modifier.fillMaxSize()) {
|
)
|
||||||
// Content
|
} else {
|
||||||
when (selectedTab) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
0 -> WalletOverview(
|
// Content
|
||||||
viewModel = walletViewModel,
|
when (selectedTab) {
|
||||||
onBackToChat = onBackToChat,
|
0 -> WalletOverview(
|
||||||
modifier = Modifier.weight(1f)
|
viewModel = walletViewModel,
|
||||||
)
|
onBackToChat = onBackToChat,
|
||||||
1 -> TransactionHistory(
|
modifier = Modifier.weight(1f)
|
||||||
transactions = walletViewModel.getAllTransactions().observeAsState(initial = emptyList()).value,
|
)
|
||||||
modifier = Modifier.weight(1f),
|
1 -> TransactionHistory(
|
||||||
onTransactionClick = { transaction ->
|
transactions = walletViewModel.getAllTransactions().observeAsState(initial = emptyList()).value,
|
||||||
// For lightning receive transactions, open the receive view with the quote
|
modifier = Modifier.weight(1f),
|
||||||
if (transaction.type == com.bitchat.android.wallet.data.TransactionType.LIGHTNING_RECEIVE &&
|
onTransactionClick = { transaction ->
|
||||||
transaction.quote != null) {
|
// For lightning receive transactions, open the receive view with the quote
|
||||||
walletViewModel.setCurrentMintQuote(transaction.quote!!)
|
if (transaction.type == com.bitchat.android.wallet.data.TransactionType.LIGHTNING_RECEIVE &&
|
||||||
showReceiveView = true
|
transaction.quote != null) {
|
||||||
|
walletViewModel.setCurrentMintQuote(transaction.quote!!)
|
||||||
|
showReceiveView = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
2 -> MintsScreen(viewModel = walletViewModel, modifier = Modifier.weight(1f))
|
||||||
2 -> MintsScreen(viewModel = walletViewModel, modifier = Modifier.weight(1f))
|
3 -> WalletSettings(
|
||||||
3 -> WalletSettings(
|
viewModel = walletViewModel,
|
||||||
viewModel = walletViewModel,
|
onBackClick = { /* No back action needed in tab navigation */ }
|
||||||
onBackClick = { /* No back action needed in tab navigation */ }
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom Navigation
|
||||||
|
WalletBottomNavigation(
|
||||||
|
selectedTab = selectedTab,
|
||||||
|
onTabSelected = { selectedTab = it }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Bottom Navigation
|
|
||||||
WalletBottomNavigation(
|
// Success animation overlay
|
||||||
selectedTab = selectedTab,
|
if (showSuccessAnimation && successAnimationData != null) {
|
||||||
onTabSelected = { selectedTab = it }
|
SuccessAnimation(
|
||||||
|
animationData = successAnimationData!!,
|
||||||
|
onAnimationComplete = {
|
||||||
|
walletViewModel.hideSuccessAnimation()
|
||||||
|
},
|
||||||
|
modifier = Modifier.zIndex(10f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle dialog states
|
// Handle dialog states - sync local view state with ViewModel dialog state
|
||||||
if (showSendDialog) {
|
LaunchedEffect(showSendDialog) {
|
||||||
LaunchedEffect(showSendDialog) {
|
if (showSendDialog) {
|
||||||
if (showSendDialog) {
|
showSendView = true
|
||||||
showSendView = true
|
} else {
|
||||||
}
|
showSendView = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showReceiveDialog) {
|
LaunchedEffect(showReceiveDialog) {
|
||||||
LaunchedEffect(showReceiveDialog) {
|
if (showReceiveDialog) {
|
||||||
if (showReceiveDialog) {
|
showReceiveView = true
|
||||||
showReceiveView = true
|
} else {
|
||||||
}
|
showReceiveView = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,12 +82,40 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
private val _currentMeltQuote = MutableLiveData<MeltQuote?>(null)
|
private val _currentMeltQuote = MutableLiveData<MeltQuote?>(null)
|
||||||
val currentMeltQuote: LiveData<MeltQuote?> = _currentMeltQuote
|
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
|
// State management
|
||||||
private var pollingJob: kotlinx.coroutines.Job? = null
|
private var pollingJob: kotlinx.coroutines.Job? = null
|
||||||
|
|
||||||
enum class SendType { CASHU, LIGHTNING }
|
enum class SendType { CASHU, LIGHTNING }
|
||||||
enum class ReceiveType { 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 {
|
init {
|
||||||
loadInitialData()
|
loadInitialData()
|
||||||
startPolling()
|
startPolling()
|
||||||
@@ -359,6 +387,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
_decodedToken.value = null
|
_decodedToken.value = null
|
||||||
_currentMintQuote.value = null
|
_currentMintQuote.value = null
|
||||||
_receiveType.value = ReceiveType.CASHU
|
_receiveType.value = ReceiveType.CASHU
|
||||||
|
clearTokenInput()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSendType(type: SendType) {
|
fun setSendType(type: SendType) {
|
||||||
@@ -381,6 +410,30 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
_errorMessage.value = null
|
_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
|
// Back navigation handler
|
||||||
private var backHandler: (() -> Boolean)? = null
|
private var backHandler: (() -> Boolean)? = null
|
||||||
|
|
||||||
@@ -488,7 +541,23 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
repository.saveTransaction(transaction).onSuccess {
|
repository.saveTransaction(transaction).onSuccess {
|
||||||
loadTransactions()
|
loadTransactions()
|
||||||
refreshBalance()
|
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()
|
hideReceiveDialog()
|
||||||
|
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
Log.e(TAG, "Failed to save transaction", error)
|
Log.e(TAG, "Failed to save transaction", error)
|
||||||
_errorMessage.value = "Failed to save transaction: ${error.message}"
|
_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)
|
* Get full transaction history (not just last 10)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user