This commit is contained in:
callebtc
2025-07-15 16:16:51 +02:00
parent df377b8ee6
commit 1ba14a00d4
8 changed files with 621 additions and 17 deletions
@@ -34,6 +34,9 @@ import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryStatus import com.bitchat.android.model.DeliveryStatus
import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.parsing.ParsedCashuToken import com.bitchat.android.parsing.ParsedCashuToken
import com.bitchat.android.ui.payment.PaymentStatusIndicator
import com.bitchat.android.ui.payment.PaymentStatus
import com.bitchat.android.wallet.viewmodel.WalletViewModel
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -51,9 +54,17 @@ import java.util.*
@Composable @Composable
fun ChatScreen( fun ChatScreen(
viewModel: ChatViewModel, viewModel: ChatViewModel,
walletViewModel: WalletViewModel? = null,
onWalletClick: () -> Unit = {}, onWalletClick: () -> Unit = {},
onWalletClickWithToken: ((com.bitchat.android.parsing.ParsedCashuToken) -> Unit)? = null onWalletClickWithToken: ((com.bitchat.android.parsing.ParsedCashuToken) -> Unit)? = null
) { ) {
// Initialize payment manager when wallet ViewModel is available
LaunchedEffect(walletViewModel) {
walletViewModel?.let { wallet ->
viewModel.initializePaymentManager(wallet)
}
}
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val messages by viewModel.messages.observeAsState(emptyList()) val messages by viewModel.messages.observeAsState(emptyList())
val connectedPeers by viewModel.connectedPeers.observeAsState(emptyList()) val connectedPeers by viewModel.connectedPeers.observeAsState(emptyList())
@@ -70,6 +81,9 @@ fun ChatScreen(
val commandSuggestions by viewModel.commandSuggestions.observeAsState(emptyList()) val commandSuggestions by viewModel.commandSuggestions.observeAsState(emptyList())
val showAppInfo by viewModel.showAppInfo.observeAsState(false) val showAppInfo by viewModel.showAppInfo.observeAsState(false)
// Observe payment status
val paymentStatus by viewModel.getPaymentStatus().collectAsState()
var messageText by remember { mutableStateOf("") } var messageText by remember { mutableStateOf("") }
var showPasswordPrompt by remember { mutableStateOf(false) } var showPasswordPrompt by remember { mutableStateOf(false) }
var showPasswordDialog by remember { mutableStateOf(false) } var showPasswordDialog by remember { mutableStateOf(false) }
@@ -139,7 +153,9 @@ fun ChatScreen(
selectedPrivatePeer = selectedPrivatePeer, selectedPrivatePeer = selectedPrivatePeer,
currentChannel = currentChannel, currentChannel = currentChannel,
nickname = nickname, nickname = nickname,
colorScheme = colorScheme colorScheme = colorScheme,
paymentStatus = paymentStatus,
onClearPaymentStatus = { viewModel.clearPaymentStatus() }
) )
} }
@@ -214,7 +230,9 @@ private fun ChatInputSection(
selectedPrivatePeer: String?, selectedPrivatePeer: String?,
currentChannel: String?, currentChannel: String?,
nickname: String, nickname: String,
colorScheme: ColorScheme colorScheme: ColorScheme,
paymentStatus: PaymentStatus?,
onClearPaymentStatus: () -> Unit
) { ) {
Surface( Surface(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -235,6 +253,13 @@ private fun ChatInputSection(
Divider(color = colorScheme.outline.copy(alpha = 0.2f)) Divider(color = colorScheme.outline.copy(alpha = 0.2f))
} }
// Payment status indicator
PaymentStatusIndicator(
status = paymentStatus,
onDismiss = onClearPaymentStatus,
modifier = Modifier.fillMaxWidth()
)
MessageInput( MessageInput(
value = messageText, value = messageText,
onValueChange = onMessageTextChange, onValueChange = onMessageTextChange,
@@ -6,19 +6,25 @@ import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitchat.android.mesh.BluetoothMeshDelegate
import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.mesh.BluetoothMeshDelegate
import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryAck import com.bitchat.android.model.DeliveryAck
import com.bitchat.android.model.ReadReceipt import com.bitchat.android.model.ReadReceipt
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.ui.payment.PaymentManager
import com.bitchat.android.ui.payment.PaymentStatus
import com.bitchat.android.wallet.viewmodel.WalletViewModel
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import java.util.* import java.util.*
import kotlin.random.Random import kotlin.random.Random
/** /**
* Refactored ChatViewModel - Main coordinator for bitchat functionality * ViewModel for managing the chat functionality
* Delegates specific responsibilities to specialized managers while maintaining 100% iOS compatibility * Refactored to use composition over inheritance for better code organization
*/ */
class ChatViewModel( class ChatViewModel(
application: Application, application: Application,
@@ -38,6 +44,14 @@ class ChatViewModel(
private val commandProcessor = CommandProcessor(state, messageManager, channelManager, privateChatManager) private val commandProcessor = CommandProcessor(state, messageManager, channelManager, privateChatManager)
private val notificationManager = NotificationManager(application.applicationContext) private val notificationManager = NotificationManager(application.applicationContext)
// Payment manager (lazily initialized with wallet ViewModel)
private var paymentManager: PaymentManager? = null
// Initialize payment manager with wallet ViewModel
fun initializePaymentManager(walletViewModel: WalletViewModel) {
paymentManager = PaymentManager(viewModelScope, walletViewModel)
}
// Delegate handler for mesh callbacks // Delegate handler for mesh callbacks
private val meshDelegateHandler = MeshDelegateHandler( private val meshDelegateHandler = MeshDelegateHandler(
state = state, state = state,
@@ -176,9 +190,17 @@ class ChatViewModel(
// Check for commands // Check for commands
if (content.startsWith("/")) { if (content.startsWith("/")) {
commandProcessor.processCommand(content, meshService, meshService.myPeerID) { messageContent, mentions, channel -> commandProcessor.processCommand(
command = content,
meshService = meshService,
myPeerID = meshService.myPeerID,
onSendMessage = { messageContent, mentions, channel ->
meshService.sendMessage(messageContent, mentions, channel) meshService.sendMessage(messageContent, mentions, channel)
},
onPaymentRequest = { amount ->
handlePaymentRequest(amount)
} }
)
return return
} }
@@ -248,6 +270,72 @@ class ChatViewModel(
} }
} }
// MARK: - Payment Management
/**
* Handle payment request from /pay command
*/
private fun handlePaymentRequest(amount: Long) {
paymentManager?.createPayment(amount) { token ->
// Send the created token to the current chat
sendCashuTokenToChat(token)
}
}
/**
* Send a Cashu token to the current chat
*/
private fun sendCashuTokenToChat(token: String) {
val selectedPeer = state.getSelectedPrivateChatPeerValue()
val currentChannelValue = state.getCurrentChannelValue()
if (selectedPeer != null) {
// Send token as private message
val recipientNickname = meshService.getPeerNicknames()[selectedPeer]
privateChatManager.sendPrivateMessage(
token,
selectedPeer,
recipientNickname,
state.getNicknameValue(),
meshService.myPeerID
) { messageContent, peerID, recipientNicknameParam, messageId ->
meshService.sendPrivateMessage(messageContent, peerID, recipientNicknameParam, messageId)
}
} else {
// Send token as public/channel message
val message = BitchatMessage(
sender = state.getNicknameValue() ?: meshService.myPeerID,
content = token,
timestamp = Date(),
isRelay = false,
senderPeerID = meshService.myPeerID,
channel = currentChannelValue
)
if (currentChannelValue != null) {
channelManager.addChannelMessage(currentChannelValue, message, meshService.myPeerID)
meshService.sendMessage(token, emptyList(), currentChannelValue)
} else {
messageManager.addMessage(message)
meshService.sendMessage(token, emptyList(), null)
}
}
}
/**
* Get payment status for UI
*/
fun getPaymentStatus(): StateFlow<PaymentStatus?> {
return paymentManager?.paymentStatus ?: kotlinx.coroutines.flow.MutableStateFlow(null).asStateFlow()
}
/**
* Clear payment status
*/
fun clearPaymentStatus() {
paymentManager?.clearStatus()
}
// MARK: - Utility Functions // MARK: - Utility Functions
fun getPeerIDForNickname(nickname: String): String? { fun getPeerIDForNickname(nickname: String): String? {
@@ -21,6 +21,7 @@ class CommandProcessor(
CommandSuggestion("/hug", emptyList(), "<nickname>", "send someone a warm hug"), CommandSuggestion("/hug", emptyList(), "<nickname>", "send someone a warm hug"),
CommandSuggestion("/j", listOf("/join"), "<channel>", "join or create a channel"), CommandSuggestion("/j", listOf("/join"), "<channel>", "join or create a channel"),
CommandSuggestion("/m", listOf("/msg"), "<nickname> [message]", "send private message"), CommandSuggestion("/m", listOf("/msg"), "<nickname> [message]", "send private message"),
CommandSuggestion("/pay", emptyList(), "<amount>", "send Cashu payment"),
CommandSuggestion("/slap", emptyList(), "<nickname>", "slap someone with a trout"), CommandSuggestion("/slap", emptyList(), "<nickname>", "slap someone with a trout"),
CommandSuggestion("/unblock", emptyList(), "<nickname>", "unblock a peer"), CommandSuggestion("/unblock", emptyList(), "<nickname>", "unblock a peer"),
CommandSuggestion("/w", emptyList(), null, "see who's online") CommandSuggestion("/w", emptyList(), null, "see who's online")
@@ -28,7 +29,13 @@ class CommandProcessor(
// MARK: - Command Processing // MARK: - Command Processing
fun processCommand(command: String, meshService: Any, myPeerID: String, onSendMessage: (String, List<String>, String?) -> Unit): Boolean { fun processCommand(
command: String,
meshService: Any,
myPeerID: String,
onSendMessage: (String, List<String>, String?) -> Unit,
onPaymentRequest: ((Long) -> Unit)? = null
): Boolean {
if (!command.startsWith("/")) return false if (!command.startsWith("/")) return false
val parts = command.split(" ") val parts = command.split(" ")
@@ -45,6 +52,7 @@ class CommandProcessor(
"/hug" -> handleActionCommand(parts, "gives", "a warm hug 🫂", meshService, myPeerID, onSendMessage) "/hug" -> handleActionCommand(parts, "gives", "a warm hug 🫂", meshService, myPeerID, onSendMessage)
"/slap" -> handleActionCommand(parts, "slaps", "around a bit with a large trout 🐟", meshService, myPeerID, onSendMessage) "/slap" -> handleActionCommand(parts, "slaps", "around a bit with a large trout 🐟", meshService, myPeerID, onSendMessage)
"/channels" -> handleChannelsCommand() "/channels" -> handleChannelsCommand()
"/pay" -> handlePayCommand(parts, onPaymentRequest)
else -> handleUnknownCommand(cmd) else -> handleUnknownCommand(cmd)
} }
@@ -316,6 +324,47 @@ class CommandProcessor(
messageManager.addMessage(systemMessage) messageManager.addMessage(systemMessage)
} }
private fun handlePayCommand(parts: List<String>, onPaymentRequest: ((Long) -> Unit)?) {
if (parts.size < 2) {
val systemMessage = BitchatMessage(
sender = "system",
content = "usage: /pay <amount> - send Cashu payment in sats",
timestamp = Date(),
isRelay = false
)
messageManager.addMessage(systemMessage)
return
}
val amountStr = parts[1]
val amount = amountStr.toLongOrNull()
if (amount == null || amount <= 0) {
val systemMessage = BitchatMessage(
sender = "system",
content = "invalid amount: '$amountStr'. please enter a positive number of sats.",
timestamp = Date(),
isRelay = false
)
messageManager.addMessage(systemMessage)
return
}
if (amount > 1_000_000) {
val systemMessage = BitchatMessage(
sender = "system",
content = "amount too large: $amount sats. maximum is 1,000,000 sats.",
timestamp = Date(),
isRelay = false
)
messageManager.addMessage(systemMessage)
return
}
// Trigger payment creation
onPaymentRequest?.invoke(amount)
}
private fun handleUnknownCommand(cmd: String) { private fun handleUnknownCommand(cmd: String) {
val systemMessage = BitchatMessage( val systemMessage = BitchatMessage(
sender = "system", sender = "system",
@@ -58,6 +58,7 @@ fun MainAppScreen(
// Chat is always the base view // Chat is always the base view
ChatScreen( ChatScreen(
viewModel = chatViewModel, viewModel = chatViewModel,
walletViewModel = walletViewModel,
onWalletClick = { showWallet = true }, onWalletClick = { showWallet = true },
onWalletClickWithToken = { parsedToken -> onWalletClickWithToken = { parsedToken ->
// Open wallet and show receive dialog with the parsed token immediately // Open wallet and show receive dialog with the parsed token immediately
@@ -0,0 +1,93 @@
package com.bitchat.android.ui.payment
import android.util.Log
import androidx.lifecycle.viewModelScope
import com.bitchat.android.wallet.viewmodel.WalletViewModel
import com.bitchat.android.wallet.data.TransactionType
import com.bitchat.android.wallet.data.TransactionStatus
import com.bitchat.android.wallet.data.WalletTransaction
import com.bitchat.android.model.BitchatMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.util.*
/**
* Manages payment creation and status for the /pay command
* Coordinates between ChatViewModel, WalletViewModel, and UI
*/
class PaymentManager(
private val coroutineScope: CoroutineScope,
private val walletViewModel: WalletViewModel
) {
companion object {
private const val TAG = "PaymentManager"
}
// Payment status state
private val _paymentStatus = MutableStateFlow<PaymentStatus?>(null)
val paymentStatus: StateFlow<PaymentStatus?> = _paymentStatus.asStateFlow()
/**
* Create a Cashu token payment for the specified amount
*/
fun createPayment(
amount: Long,
onTokenCreated: (String) -> Unit
) {
Log.d(TAG, "Creating payment for $amount sats")
// Set creating status
_paymentStatus.value = PaymentStatus.Creating(amount)
coroutineScope.launch {
try {
// Use WalletViewModel's createCashuToken method
walletViewModel.createCashuTokenForPayment(
amount = amount,
memo = "Payment via /pay command",
onSuccess = { token ->
Log.d(TAG, "Payment token created successfully: ${token.take(20)}...")
// Set success status
_paymentStatus.value = PaymentStatus.Success(amount, token)
// Notify caller with the token
onTokenCreated(token)
},
onError = { error ->
Log.e(TAG, "Payment creation failed: $error")
// Set error status
_paymentStatus.value = PaymentStatus.Error(amount, error)
}
)
} catch (e: Exception) {
Log.e(TAG, "Exception creating payment", e)
_paymentStatus.value = PaymentStatus.Error(
amount,
e.message ?: "Unknown error occurred"
)
}
}
}
/**
* Clear the current payment status
*/
fun clearStatus() {
_paymentStatus.value = null
}
/**
* Check if a payment is currently being created
*/
fun isCreatingPayment(): Boolean {
return _paymentStatus.value is PaymentStatus.Creating
}
}
@@ -0,0 +1,298 @@
package com.bitchat.android.ui.payment
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
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.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
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.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
/**
* Payment status for Cashu token creation
*/
sealed class PaymentStatus {
data class Creating(val amount: Long) : PaymentStatus()
data class Success(val amount: Long, val token: String) : PaymentStatus()
data class Error(val amount: Long, val message: String) : PaymentStatus()
}
/**
* Sleek payment status indicator that slides up above the text input
* Shows creation progress, success, and error states
*/
@Composable
fun PaymentStatusIndicator(
status: PaymentStatus?,
onDismiss: () -> Unit,
modifier: Modifier = Modifier
) {
val isVisible = status != null
// Auto-dismiss success/error states after delay
LaunchedEffect(status) {
if (status is PaymentStatus.Success || status is PaymentStatus.Error) {
delay(3000) // Show for 3 seconds
onDismiss()
}
}
AnimatedVisibility(
visible = isVisible,
enter = slideInVertically(
initialOffsetY = { it },
animationSpec = spring(
dampingRatio = 0.8f,
stiffness = Spring.StiffnessMedium
)
) + fadeIn(
animationSpec = tween(300, easing = EaseOutCubic)
),
exit = slideOutVertically(
targetOffsetY = { it },
animationSpec = spring(
dampingRatio = 0.9f,
stiffness = Spring.StiffnessMedium
)
) + fadeOut(
animationSpec = tween(200, easing = EaseInCubic)
),
modifier = modifier
) {
if (status != null) {
PaymentStatusCard(status = status)
}
}
}
@Composable
private fun PaymentStatusCard(status: PaymentStatus) {
val colors = when (status) {
is PaymentStatus.Creating -> PaymentColors(
background = Color(0xFF1A1A1A),
border = Color(0xFF00C851),
icon = Color(0xFF00C851),
text = Color.White
)
is PaymentStatus.Success -> PaymentColors(
background = Color(0xFF00C851).copy(alpha = 0.15f),
border = Color(0xFF00C851),
icon = Color(0xFF00C851),
text = Color.White
)
is PaymentStatus.Error -> PaymentColors(
background = Color(0xFFFF4444).copy(alpha = 0.15f),
border = Color(0xFFFF4444),
icon = Color(0xFFFF4444),
text = Color.White
)
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = colors.background
),
border = androidx.compose.foundation.BorderStroke(2.dp, colors.border)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Status icon with animation
PaymentStatusIcon(
status = status,
color = colors.icon
)
// Status content
Column(
modifier = Modifier.weight(1f)
) {
PaymentStatusText(
status = status,
textColor = colors.text
)
}
// Amount display
PaymentAmountDisplay(
amount = when (status) {
is PaymentStatus.Creating -> status.amount
is PaymentStatus.Success -> status.amount
is PaymentStatus.Error -> status.amount
},
color = colors.text
)
}
}
}
@Composable
private fun PaymentStatusIcon(
status: PaymentStatus,
color: Color
) {
when (status) {
is PaymentStatus.Creating -> {
// Animated spinner
val infiniteTransition = rememberInfiniteTransition(label = "payment_creating")
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "spinner_rotation"
)
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = "Creating payment",
tint = color,
modifier = Modifier
.size(24.dp)
.graphicsLayer { rotationZ = rotation }
)
}
is PaymentStatus.Success -> {
Icon(
imageVector = Icons.Filled.CheckCircle,
contentDescription = "Payment sent",
tint = color,
modifier = Modifier.size(24.dp)
)
}
is PaymentStatus.Error -> {
Icon(
imageVector = Icons.Filled.Error,
contentDescription = "Payment failed",
tint = color,
modifier = Modifier.size(24.dp)
)
}
}
}
@Composable
private fun PaymentStatusText(
status: PaymentStatus,
textColor: Color
) {
when (status) {
is PaymentStatus.Creating -> {
Text(
text = "Creating payment...",
color = textColor,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
fontFamily = FontFamily.Monospace
)
Text(
text = "Generating Cashu token",
color = textColor.copy(alpha = 0.7f),
fontSize = 12.sp,
fontFamily = FontFamily.Monospace
)
}
is PaymentStatus.Success -> {
Text(
text = "Payment sent!",
color = textColor,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
Text(
text = "Cashu token delivered to chat",
color = textColor.copy(alpha = 0.8f),
fontSize = 12.sp,
fontFamily = FontFamily.Monospace
)
}
is PaymentStatus.Error -> {
Text(
text = "Payment failed",
color = textColor,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
Text(
text = status.message,
color = textColor.copy(alpha = 0.8f),
fontSize = 12.sp,
fontFamily = FontFamily.Monospace
)
}
}
}
@Composable
private fun PaymentAmountDisplay(
amount: Long,
color: Color
) {
Column(
horizontalAlignment = Alignment.End
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "",
fontSize = 16.sp,
color = color
)
Text(
text = formatSats(amount),
color = color,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
}
}
}
/**
* Colors for different payment states
*/
private data class PaymentColors(
val background: Color,
val border: Color,
val icon: Color,
val text: Color
)
/**
* Format sats amount for display
*/
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"
}
}
@@ -284,7 +284,9 @@ class CashuService {
Log.d(TAG, "Token validation successful, amount: $amount") Log.d(TAG, "Token validation successful, amount: $amount")
wallet!!.receive(token) val ffiAmount = wallet!!.receive(token)
Log.d(TAG, "FFI amount: ${ffiAmount}")
Result.success(amount) Result.success(amount)
@@ -526,6 +526,54 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
} }
} }
/**
* Create a Cashu token for payments (with callbacks for /pay command)
*/
fun createCashuTokenForPayment(
amount: Long,
memo: String? = null,
onSuccess: (String) -> Unit,
onError: (String) -> Unit
) {
viewModelScope.launch {
try {
cashuService.createToken(amount, memo).onSuccess { token ->
// Add transaction
val transaction = WalletTransaction(
id = UUID.randomUUID().toString(),
type = TransactionType.CASHU_SEND,
amount = BigDecimal(amount),
unit = "sat",
status = TransactionStatus.CONFIRMED,
timestamp = Date(),
description = memo ?: "Cashu token sent",
token = token
)
repository.saveTransaction(transaction).onSuccess {
loadTransactions()
refreshBalance()
// Success callback with token
onSuccess(token)
}.onFailure { error ->
Log.e(TAG, "Failed to save transaction", error)
onError("Failed to save transaction: ${error.message}")
}
}.onFailure { error ->
onError("Failed to create token: ${error.message}")
}
} catch (e: Exception) {
Log.e(TAG, "Exception creating payment token", e)
onError(e.message ?: "Unknown error occurred")
}
}
}
/** /**
* Decode a Cashu token to show information * Decode a Cashu token to show information
*/ */