diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt index ec487d5b..75576da6 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt @@ -34,6 +34,9 @@ import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.DeliveryStatus import com.bitchat.android.mesh.BluetoothMeshService 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.util.* @@ -51,9 +54,17 @@ import java.util.* @Composable fun ChatScreen( viewModel: ChatViewModel, + walletViewModel: WalletViewModel? = null, onWalletClick: () -> Unit = {}, 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 messages by viewModel.messages.observeAsState(emptyList()) val connectedPeers by viewModel.connectedPeers.observeAsState(emptyList()) @@ -70,6 +81,9 @@ fun ChatScreen( val commandSuggestions by viewModel.commandSuggestions.observeAsState(emptyList()) val showAppInfo by viewModel.showAppInfo.observeAsState(false) + // Observe payment status + val paymentStatus by viewModel.getPaymentStatus().collectAsState() + var messageText by remember { mutableStateOf("") } var showPasswordPrompt by remember { mutableStateOf(false) } var showPasswordDialog by remember { mutableStateOf(false) } @@ -139,7 +153,9 @@ fun ChatScreen( selectedPrivatePeer = selectedPrivatePeer, currentChannel = currentChannel, nickname = nickname, - colorScheme = colorScheme + colorScheme = colorScheme, + paymentStatus = paymentStatus, + onClearPaymentStatus = { viewModel.clearPaymentStatus() } ) } @@ -214,7 +230,9 @@ private fun ChatInputSection( selectedPrivatePeer: String?, currentChannel: String?, nickname: String, - colorScheme: ColorScheme + colorScheme: ColorScheme, + paymentStatus: PaymentStatus?, + onClearPaymentStatus: () -> Unit ) { Surface( modifier = Modifier.fillMaxWidth(), @@ -235,6 +253,13 @@ private fun ChatInputSection( Divider(color = colorScheme.outline.copy(alpha = 0.2f)) } + // Payment status indicator + PaymentStatusIndicator( + status = paymentStatus, + onDismiss = onClearPaymentStatus, + modifier = Modifier.fillMaxWidth() + ) + MessageInput( value = messageText, onValueChange = onMessageTextChange, diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 190f696d..b5b509fb 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -6,19 +6,25 @@ import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope -import com.bitchat.android.mesh.BluetoothMeshDelegate import com.bitchat.android.mesh.BluetoothMeshService +import com.bitchat.android.mesh.BluetoothMeshDelegate import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.DeliveryAck 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.delay import java.util.* import kotlin.random.Random /** - * Refactored ChatViewModel - Main coordinator for bitchat functionality - * Delegates specific responsibilities to specialized managers while maintaining 100% iOS compatibility + * ViewModel for managing the chat functionality + * Refactored to use composition over inheritance for better code organization */ class ChatViewModel( application: Application, @@ -38,6 +44,14 @@ class ChatViewModel( private val commandProcessor = CommandProcessor(state, messageManager, channelManager, privateChatManager) 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 private val meshDelegateHandler = MeshDelegateHandler( state = state, @@ -170,31 +184,39 @@ class ChatViewModel( } // MARK: - Message Sending - + fun sendMessage(content: String) { if (content.isEmpty()) return - + // Check for commands if (content.startsWith("/")) { - commandProcessor.processCommand(content, meshService, meshService.myPeerID) { messageContent, mentions, channel -> - meshService.sendMessage(messageContent, mentions, channel) - } + commandProcessor.processCommand( + command = content, + meshService = meshService, + myPeerID = meshService.myPeerID, + onSendMessage = { messageContent, mentions, channel -> + meshService.sendMessage(messageContent, mentions, channel) + }, + onPaymentRequest = { amount -> + handlePaymentRequest(amount) + } + ) return } - + val mentions = messageManager.parseMentions(content, meshService.getPeerNicknames().values.toSet(), state.getNicknameValue()) val channels = messageManager.parseChannels(content) - + // Auto-join mentioned channels channels.forEach { channel -> if (!state.getJoinedChannelsValue().contains(channel)) { joinChannel(channel) } } - + val selectedPeer = state.getSelectedPrivateChatPeerValue() val currentChannelValue = state.getCurrentChannelValue() - + if (selectedPeer != null) { // Send private message val recipientNickname = meshService.getPeerNicknames()[selectedPeer] @@ -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 { + return paymentManager?.paymentStatus ?: kotlinx.coroutines.flow.MutableStateFlow(null).asStateFlow() + } + + /** + * Clear payment status + */ + fun clearPaymentStatus() { + paymentManager?.clearStatus() + } + // MARK: - Utility Functions fun getPeerIDForNickname(nickname: String): String? { diff --git a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt index bec01eec..cb80dd2b 100644 --- a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt +++ b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt @@ -21,6 +21,7 @@ class CommandProcessor( CommandSuggestion("/hug", emptyList(), "", "send someone a warm hug"), CommandSuggestion("/j", listOf("/join"), "", "join or create a channel"), CommandSuggestion("/m", listOf("/msg"), " [message]", "send private message"), + CommandSuggestion("/pay", emptyList(), "", "send Cashu payment"), CommandSuggestion("/slap", emptyList(), "", "slap someone with a trout"), CommandSuggestion("/unblock", emptyList(), "", "unblock a peer"), CommandSuggestion("/w", emptyList(), null, "see who's online") @@ -28,7 +29,13 @@ class CommandProcessor( // MARK: - Command Processing - fun processCommand(command: String, meshService: Any, myPeerID: String, onSendMessage: (String, List, String?) -> Unit): Boolean { + fun processCommand( + command: String, + meshService: Any, + myPeerID: String, + onSendMessage: (String, List, String?) -> Unit, + onPaymentRequest: ((Long) -> Unit)? = null + ): Boolean { if (!command.startsWith("/")) return false val parts = command.split(" ") @@ -45,6 +52,7 @@ class CommandProcessor( "/hug" -> handleActionCommand(parts, "gives", "a warm hug 🫂", meshService, myPeerID, onSendMessage) "/slap" -> handleActionCommand(parts, "slaps", "around a bit with a large trout 🐟", meshService, myPeerID, onSendMessage) "/channels" -> handleChannelsCommand() + "/pay" -> handlePayCommand(parts, onPaymentRequest) else -> handleUnknownCommand(cmd) } @@ -316,6 +324,47 @@ class CommandProcessor( messageManager.addMessage(systemMessage) } + private fun handlePayCommand(parts: List, onPaymentRequest: ((Long) -> Unit)?) { + if (parts.size < 2) { + val systemMessage = BitchatMessage( + sender = "system", + content = "usage: /pay - 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) { val systemMessage = BitchatMessage( sender = "system", diff --git a/app/src/main/java/com/bitchat/android/ui/MainAppScreen.kt b/app/src/main/java/com/bitchat/android/ui/MainAppScreen.kt index cd96ffa6..cb255654 100644 --- a/app/src/main/java/com/bitchat/android/ui/MainAppScreen.kt +++ b/app/src/main/java/com/bitchat/android/ui/MainAppScreen.kt @@ -58,6 +58,7 @@ fun MainAppScreen( // Chat is always the base view ChatScreen( viewModel = chatViewModel, + walletViewModel = walletViewModel, onWalletClick = { showWallet = true }, onWalletClickWithToken = { parsedToken -> // Open wallet and show receive dialog with the parsed token immediately diff --git a/app/src/main/java/com/bitchat/android/ui/payment/PaymentManager.kt b/app/src/main/java/com/bitchat/android/ui/payment/PaymentManager.kt new file mode 100644 index 00000000..9fe78454 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/ui/payment/PaymentManager.kt @@ -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(null) + val paymentStatus: StateFlow = _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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bitchat/android/ui/payment/PaymentStatusIndicator.kt b/app/src/main/java/com/bitchat/android/ui/payment/PaymentStatusIndicator.kt new file mode 100644 index 00000000..0a0480e1 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/ui/payment/PaymentStatusIndicator.kt @@ -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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bitchat/android/wallet/service/CashuService.kt b/app/src/main/java/com/bitchat/android/wallet/service/CashuService.kt index 6ac84aeb..34db01c9 100644 --- a/app/src/main/java/com/bitchat/android/wallet/service/CashuService.kt +++ b/app/src/main/java/com/bitchat/android/wallet/service/CashuService.kt @@ -284,8 +284,10 @@ class CashuService { 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) } catch (e: FfiException) { diff --git a/app/src/main/java/com/bitchat/android/wallet/viewmodel/WalletViewModel.kt b/app/src/main/java/com/bitchat/android/wallet/viewmodel/WalletViewModel.kt index 79c63a23..82fc2eec 100644 --- a/app/src/main/java/com/bitchat/android/wallet/viewmodel/WalletViewModel.kt +++ b/app/src/main/java/com/bitchat/android/wallet/viewmodel/WalletViewModel.kt @@ -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 */