mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 14:05:19 +00:00
worky
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<PaymentStatus?> {
|
||||
return paymentManager?.paymentStatus ?: kotlinx.coroutines.flow.MutableStateFlow(null).asStateFlow()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear payment status
|
||||
*/
|
||||
fun clearPaymentStatus() {
|
||||
paymentManager?.clearStatus()
|
||||
}
|
||||
|
||||
// MARK: - Utility Functions
|
||||
|
||||
fun getPeerIDForNickname(nickname: String): String? {
|
||||
|
||||
@@ -21,6 +21,7 @@ class CommandProcessor(
|
||||
CommandSuggestion("/hug", emptyList(), "<nickname>", "send someone a warm hug"),
|
||||
CommandSuggestion("/j", listOf("/join"), "<channel>", "join or create a channel"),
|
||||
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("/unblock", emptyList(), "<nickname>", "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>, 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
|
||||
|
||||
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<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) {
|
||||
val systemMessage = BitchatMessage(
|
||||
sender = "system",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user