refactor af

This commit is contained in:
callebtc
2025-07-15 18:01:37 +02:00
parent c15792c3e5
commit c4f39f7f82
8 changed files with 1118 additions and 1024 deletions
@@ -171,15 +171,15 @@ class BluetoothGattServerManager(
} }
if (characteristic.uuid == CHARACTERISTIC_UUID) { if (characteristic.uuid == CHARACTERISTIC_UUID) {
Log.i(TAG, "Server: Received packet from ${device.address}, size: ${value.size} bytes") // Log.i(TAG, "Server: Received packet from ${device.address}, size: ${value.size} bytes")
val packet = BitchatPacket.fromBinaryData(value) val packet = BitchatPacket.fromBinaryData(value)
if (packet != null) { if (packet != null) {
val peerID = String(packet.senderID).replace("\u0000", "") val peerID = String(packet.senderID).replace("\u0000", "")
Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID") Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID")
delegate?.onPacketReceived(packet, peerID, device) delegate?.onPacketReceived(packet, peerID, device)
} else { } else {
Log.w(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes") // Log.w(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes")
Log.w(TAG, "Server: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}") // Log.w(TAG, "Server: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}")
} }
if (responseNeeded) { if (responseNeeded) {
@@ -4,8 +4,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
@@ -16,16 +14,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily 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.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
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 com.bitchat.android.wallet.data.MintQuote
import com.bitchat.android.wallet.data.CashuToken
import com.bitchat.android.wallet.viewmodel.WalletViewModel import com.bitchat.android.wallet.viewmodel.WalletViewModel
/** /**
@@ -150,14 +142,14 @@ fun ReceiveView(
// Content based on receive type // Content based on receive type
when (receiveType) { when (receiveType) {
WalletViewModel.ReceiveType.CASHU -> { WalletViewModel.ReceiveType.CASHU -> {
CashuReceiveContent( ReceiveEcashDialog(
viewModel = viewModel, viewModel = viewModel,
decodedToken = decodedToken, decodedToken = decodedToken,
isLoading = isLoading isLoading = isLoading
) )
} }
WalletViewModel.ReceiveType.LIGHTNING -> { WalletViewModel.ReceiveType.LIGHTNING -> {
LightningReceiveContent( ReceiveLightningDialog(
viewModel = viewModel, viewModel = viewModel,
currentMintQuote = currentMintQuote, currentMintQuote = currentMintQuote,
isLoading = isLoading isLoading = isLoading
@@ -168,480 +160,4 @@ fun ReceiveView(
} }
} }
@Composable
private fun CashuReceiveContent(
viewModel: WalletViewModel,
decodedToken: CashuToken?,
isLoading: Boolean
) {
val token by viewModel.tokenInput.observeAsState("")
val clipboardManager = LocalClipboardManager.current
if (decodedToken != null) {
// Show decoded token info and receive button
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Token info card
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Filled.Toll,
contentDescription = "Token",
tint = Color(0xFF00C851),
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "CASHU TOKEN",
color = Color(0xFF00C851),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
Spacer(modifier = Modifier.height(24.dp))
// Amount
Text(
text = formatSats(decodedToken.amount.toLong()),
color = Color.White,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
Spacer(modifier = Modifier.height(16.dp))
// Mint info
Text(
text = "From: ${decodedToken.mint.take(30)}${if (decodedToken.mint.length > 30) "..." else ""}",
color = Color.Gray,
fontSize = 14.sp,
fontFamily = FontFamily.Monospace,
textAlign = TextAlign.Center
)
if (!decodedToken.memo.isNullOrEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Memo: ${decodedToken.memo}",
color = Color.Gray,
fontSize = 14.sp,
fontFamily = FontFamily.Monospace,
textAlign = TextAlign.Center
)
}
}
}
// Receive button
Button(
onClick = {
viewModel.receiveCashuToken(decodedToken.token)
},
enabled = !isLoading,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851),
disabledContainerColor = Color(0xFF2A2A2A)
),
shape = RoundedCornerShape(16.dp)
) {
if (isLoading) {
CircularProgressIndicator(
color = Color.Black,
modifier = Modifier.size(24.dp)
)
} else {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.Download,
contentDescription = "Receive",
tint = Color.Black,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "RECEIVE TOKEN",
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
}
} else {
// Show token input
Column(
modifier = Modifier.fillMaxWidth()
) {
// Token input field
OutlinedTextField(
value = token,
onValueChange = {
viewModel.setTokenInput(it)
},
label = {
Text(
text = "Cashu Token",
fontFamily = FontFamily.Monospace
)
},
placeholder = {
Text(
text = "cashu...",
fontFamily = FontFamily.Monospace,
color = Color.Gray
)
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF00C851),
focusedLabelColor = Color(0xFF00C851),
unfocusedBorderColor = Color(0xFF2A2A2A),
unfocusedLabelColor = Color.Gray,
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
minLines = 3,
maxLines = 4,
shape = RoundedCornerShape(16.dp),
trailingIcon = {
IconButton(
onClick = {
// TODO: Implement QR code scanning
}
) {
Icon(
imageVector = Icons.Filled.QrCode,
contentDescription = "Scan QR",
tint = Color(0xFF00C851)
)
}
}
)
// Paste button
Button(
onClick = {
clipboardManager.getText()?.text?.let { clipText ->
if (clipText.startsWith("cashu")) {
viewModel.setTokenInput(clipText)
}
}
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
border = androidx.compose.foundation.BorderStroke(
2.dp,
Color(0xFF00C851)
),
shape = RoundedCornerShape(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.ContentPaste,
contentDescription = "Paste",
tint = Color(0xFF00C851),
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "PASTE FROM CLIPBOARD",
color = Color(0xFF00C851),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
}
}
@Composable
private fun LightningReceiveContent(
viewModel: WalletViewModel,
currentMintQuote: MintQuote?,
isLoading: Boolean
) {
var amount by remember { mutableStateOf("") }
var description by remember { mutableStateOf("") }
val clipboardManager = LocalClipboardManager.current
if (currentMintQuote != null) {
// Show Lightning invoice QR code and details
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Invoice header
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Filled.FlashOn,
contentDescription = "Lightning",
tint = Color(0xFFFFB000),
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "LIGHTNING INVOICE",
color = Color(0xFFFFB000),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
Spacer(modifier = Modifier.height(24.dp))
// Amount
Text(
text = formatSats(currentMintQuote.amount.toLong()),
color = Color.White,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
Spacer(modifier = Modifier.height(16.dp))
// Status
Row(
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(12.dp)
.clip(RoundedCornerShape(6.dp))
.background(if (currentMintQuote.paid) Color(0xFF00C851) else Color(0xFFFFB000))
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = if (currentMintQuote.paid) "PAID" else "WAITING FOR PAYMENT",
color = if (currentMintQuote.paid) Color(0xFF00C851) else Color(0xFFFFB000),
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
// QR Code
Card(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.padding(bottom = 24.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
QRCodeCanvas(
text = currentMintQuote.request,
modifier = Modifier.fillMaxSize()
)
}
}
// Copy invoice button
Button(
onClick = {
clipboardManager.setText(AnnotatedString(currentMintQuote.request))
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
border = androidx.compose.foundation.BorderStroke(
2.dp,
Color(0xFFFFB000)
),
shape = RoundedCornerShape(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.ContentCopy,
contentDescription = "Copy",
tint = Color(0xFFFFB000),
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "COPY INVOICE",
color = Color(0xFFFFB000),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
} else {
// Show amount input
Column(
modifier = Modifier.fillMaxWidth()
) {
// Amount input
OutlinedTextField(
value = amount,
onValueChange = { amount = it },
label = {
Text(
text = "Amount (sats)",
fontFamily = FontFamily.Monospace
)
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF00C851),
focusedLabelColor = Color(0xFF00C851),
unfocusedBorderColor = Color(0xFF2A2A2A),
unfocusedLabelColor = Color.Gray,
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
shape = RoundedCornerShape(16.dp)
)
// Description input
OutlinedTextField(
value = description,
onValueChange = { description = it },
label = {
Text(
text = "Description (optional)",
fontFamily = FontFamily.Monospace
)
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF00C851),
focusedLabelColor = Color(0xFF00C851),
unfocusedBorderColor = Color(0xFF2A2A2A),
unfocusedLabelColor = Color.Gray,
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp),
shape = RoundedCornerShape(16.dp)
)
// Create invoice button
Button(
onClick = {
amount.toLongOrNull()?.let { amountSats ->
viewModel.createMintQuote(amountSats, description.ifEmpty { null })
}
},
enabled = !isLoading && amount.toLongOrNull()?.let { it > 0 } == true,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851),
disabledContainerColor = Color(0xFF2A2A2A)
),
shape = RoundedCornerShape(16.dp)
) {
if (isLoading) {
CircularProgressIndicator(
color = Color.Black,
modifier = Modifier.size(24.dp)
)
} else {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.FlashOn,
contentDescription = "Create Invoice",
tint = Color.Black,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "CREATE INVOICE",
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
}
}
}
// 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"
}
}
@@ -0,0 +1,253 @@
package com.bitchat.android.wallet.ui
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
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.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
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.data.CashuToken
import com.bitchat.android.wallet.viewmodel.WalletViewModel
/**
* Ecash (Cashu) token receive dialog content
*/
@Composable
fun ReceiveEcashDialog(
viewModel: WalletViewModel,
decodedToken: CashuToken?,
isLoading: Boolean
) {
val token by viewModel.tokenInput.observeAsState("")
val clipboardManager = LocalClipboardManager.current
if (decodedToken != null) {
// Show decoded token info and receive button
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Token info card
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Filled.Toll,
contentDescription = "Token",
tint = Color(0xFF00C851),
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "CASHU TOKEN",
color = Color(0xFF00C851),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
Spacer(modifier = Modifier.height(24.dp))
// Amount
Text(
text = WalletUtils.formatSats(decodedToken.amount.toLong()),
color = Color.White,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
Spacer(modifier = Modifier.height(16.dp))
// Mint info
Text(
text = "From: ${decodedToken.mint.take(30)}${if (decodedToken.mint.length > 30) "..." else ""}",
color = Color.Gray,
fontSize = 14.sp,
fontFamily = FontFamily.Monospace,
textAlign = TextAlign.Center
)
if (!decodedToken.memo.isNullOrEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Memo: ${decodedToken.memo}",
color = Color.Gray,
fontSize = 14.sp,
fontFamily = FontFamily.Monospace,
textAlign = TextAlign.Center
)
}
}
}
// Receive button
Button(
onClick = {
viewModel.receiveCashuToken(decodedToken.token)
},
enabled = !isLoading,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851),
disabledContainerColor = Color(0xFF2A2A2A)
),
shape = RoundedCornerShape(16.dp)
) {
if (isLoading) {
CircularProgressIndicator(
color = Color.Black,
modifier = Modifier.size(24.dp)
)
} else {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.Download,
contentDescription = "Receive",
tint = Color.Black,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "RECEIVE TOKEN",
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
}
} else {
// Show token input
Column(
modifier = Modifier.fillMaxWidth()
) {
// Token input field
OutlinedTextField(
value = token,
onValueChange = {
viewModel.setTokenInput(it)
},
label = {
Text(
text = "Cashu Token",
fontFamily = FontFamily.Monospace
)
},
placeholder = {
Text(
text = "cashu...",
fontFamily = FontFamily.Monospace,
color = Color.Gray
)
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF00C851),
focusedLabelColor = Color(0xFF00C851),
unfocusedBorderColor = Color(0xFF2A2A2A),
unfocusedLabelColor = Color.Gray,
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
minLines = 3,
maxLines = 4,
shape = RoundedCornerShape(16.dp),
trailingIcon = {
IconButton(
onClick = {
// TODO: Implement QR code scanning
}
) {
Icon(
imageVector = Icons.Filled.QrCode,
contentDescription = "Scan QR",
tint = Color(0xFF00C851)
)
}
}
)
// Paste button
Button(
onClick = {
clipboardManager.getText()?.text?.let { clipText ->
if (clipText.startsWith("cashu")) {
viewModel.setTokenInput(clipText)
}
}
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
border = BorderStroke(
2.dp,
Color(0xFF00C851)
),
shape = RoundedCornerShape(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.ContentPaste,
contentDescription = "Paste",
tint = Color(0xFF00C851),
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "PASTE FROM CLIPBOARD",
color = Color(0xFF00C851),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
}
}
@@ -0,0 +1,275 @@
package com.bitchat.android.wallet.ui
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
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.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.bitchat.android.wallet.data.MintQuote
import com.bitchat.android.wallet.viewmodel.WalletViewModel
/**
* Lightning invoice receive dialog content
*/
@Composable
fun ReceiveLightningDialog(
viewModel: WalletViewModel,
currentMintQuote: MintQuote?,
isLoading: Boolean
) {
var amount by remember { mutableStateOf("") }
var description by remember { mutableStateOf("") }
val clipboardManager = LocalClipboardManager.current
if (currentMintQuote != null) {
// Show Lightning invoice QR code and details
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Invoice header
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Filled.FlashOn,
contentDescription = "Lightning",
tint = Color(0xFFFFB000),
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "LIGHTNING INVOICE",
color = Color(0xFFFFB000),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
Spacer(modifier = Modifier.height(24.dp))
// Amount
Text(
text = WalletUtils.formatSats(currentMintQuote.amount.toLong()),
color = Color.White,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
Spacer(modifier = Modifier.height(16.dp))
// Status
Row(
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(12.dp)
.clip(RoundedCornerShape(6.dp))
.background(if (currentMintQuote.paid) Color(0xFF00C851) else Color(0xFFFFB000))
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = if (currentMintQuote.paid) "PAID" else "WAITING FOR PAYMENT",
color = if (currentMintQuote.paid) Color(0xFF00C851) else Color(0xFFFFB000),
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
// QR Code
Card(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.padding(bottom = 24.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
QRCodeCanvas(
text = currentMintQuote.request,
modifier = Modifier.fillMaxSize()
)
}
}
// Copy invoice button
Button(
onClick = {
clipboardManager.setText(AnnotatedString(currentMintQuote.request))
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
border = BorderStroke(
2.dp,
Color(0xFFFFB000)
),
shape = RoundedCornerShape(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.ContentCopy,
contentDescription = "Copy",
tint = Color(0xFFFFB000),
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "COPY INVOICE",
color = Color(0xFFFFB000),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
} else {
// Show amount input
Column(
modifier = Modifier.fillMaxWidth()
) {
// Amount input
OutlinedTextField(
value = amount,
onValueChange = { amount = it },
label = {
Text(
text = "Amount (sats)",
fontFamily = FontFamily.Monospace
)
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF00C851),
focusedLabelColor = Color(0xFF00C851),
unfocusedBorderColor = Color(0xFF2A2A2A),
unfocusedLabelColor = Color.Gray,
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
shape = RoundedCornerShape(16.dp)
)
// Description input
OutlinedTextField(
value = description,
onValueChange = { description = it },
label = {
Text(
text = "Description (optional)",
fontFamily = FontFamily.Monospace
)
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF00C851),
focusedLabelColor = Color(0xFF00C851),
unfocusedBorderColor = Color(0xFF2A2A2A),
unfocusedLabelColor = Color.Gray,
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp),
shape = RoundedCornerShape(16.dp)
)
// Create invoice button
Button(
onClick = {
amount.toLongOrNull()?.let { amountSats ->
viewModel.createMintQuote(amountSats, description.ifEmpty { null })
}
},
enabled = !isLoading && amount.toLongOrNull()?.let { it > 0 } == true,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851),
disabledContainerColor = Color(0xFF2A2A2A)
),
shape = RoundedCornerShape(16.dp)
) {
if (isLoading) {
CircularProgressIndicator(
color = Color.Black,
modifier = Modifier.size(24.dp)
)
} else {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.FlashOn,
contentDescription = "Create Invoice",
tint = Color.Black,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "CREATE INVOICE",
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
}
}
}
@@ -4,8 +4,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
@@ -16,15 +14,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily 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.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
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 com.bitchat.android.wallet.data.MeltQuote
import com.bitchat.android.wallet.viewmodel.WalletViewModel import com.bitchat.android.wallet.viewmodel.WalletViewModel
/** /**
@@ -116,7 +109,7 @@ fun SendView(
) )
Text( Text(
text = formatSats(balance), text = WalletUtils.formatSats(balance),
color = Color.White, color = Color.White,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -184,7 +177,7 @@ fun SendView(
// Content based on send type // Content based on send type
when (sendType) { when (sendType) {
WalletViewModel.SendType.CASHU -> { WalletViewModel.SendType.CASHU -> {
CashuSendContent( SendEcashDialog(
viewModel = viewModel, viewModel = viewModel,
generatedToken = generatedToken, generatedToken = generatedToken,
isLoading = isLoading, isLoading = isLoading,
@@ -192,7 +185,7 @@ fun SendView(
) )
} }
WalletViewModel.SendType.LIGHTNING -> { WalletViewModel.SendType.LIGHTNING -> {
LightningSendContent( SendLightningDialog(
viewModel = viewModel, viewModel = viewModel,
currentMeltQuote = currentMeltQuote, currentMeltQuote = currentMeltQuote,
isLoading = isLoading, isLoading = isLoading,
@@ -204,529 +197,4 @@ fun SendView(
} }
} }
@Composable
private fun CashuSendContent(
viewModel: WalletViewModel,
generatedToken: String?,
isLoading: Boolean,
maxAmount: Long
) {
var amount by remember { mutableStateOf("") }
var memo by remember { mutableStateOf("") }
val clipboardManager = LocalClipboardManager.current
if (generatedToken != null) {
// Show generated token
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Token info card
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Filled.Toll,
contentDescription = "Token",
tint = Color(0xFF00C851),
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "CASHU TOKEN CREATED",
color = Color(0xFF00C851),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
Spacer(modifier = Modifier.height(24.dp))
// Token display
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF2A2A2A))
) {
SelectionContainer {
Text(
text = generatedToken,
color = Color.White,
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
modifier = Modifier.padding(16.dp),
textAlign = TextAlign.Center
)
}
}
}
}
// Copy button
Button(
onClick = {
clipboardManager.setText(AnnotatedString(generatedToken))
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851)
),
shape = RoundedCornerShape(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.ContentCopy,
contentDescription = "Copy",
tint = Color.Black,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "COPY TOKEN",
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
} else {
// Show input form
Column(
modifier = Modifier.fillMaxWidth()
) {
// Amount input
OutlinedTextField(
value = amount,
onValueChange = { amount = it },
label = {
Text(
text = "Amount (sats)",
fontFamily = FontFamily.Monospace
)
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF00C851),
focusedLabelColor = Color(0xFF00C851),
unfocusedBorderColor = Color(0xFF2A2A2A),
unfocusedLabelColor = Color.Gray,
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
shape = RoundedCornerShape(16.dp)
)
// Memo input
OutlinedTextField(
value = memo,
onValueChange = { memo = it },
label = {
Text(
text = "Memo (optional)",
fontFamily = FontFamily.Monospace
)
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF00C851),
focusedLabelColor = Color(0xFF00C851),
unfocusedBorderColor = Color(0xFF2A2A2A),
unfocusedLabelColor = Color.Gray,
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp),
shape = RoundedCornerShape(16.dp)
)
// Create token button
Button(
onClick = {
amount.toLongOrNull()?.let { amountSats ->
viewModel.createCashuToken(amountSats, memo.ifEmpty { null })
}
},
enabled = !isLoading && amount.toLongOrNull()?.let { it > 0 && it <= maxAmount } == true,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851),
disabledContainerColor = Color(0xFF2A2A2A)
),
shape = RoundedCornerShape(16.dp)
) {
if (isLoading) {
CircularProgressIndicator(
color = Color.Black,
modifier = Modifier.size(24.dp)
)
} else {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.Toll,
contentDescription = "Create Token",
tint = Color.Black,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "CREATE TOKEN",
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
}
}
}
@Composable
private fun LightningSendContent(
viewModel: WalletViewModel,
currentMeltQuote: MeltQuote?,
isLoading: Boolean,
maxAmount: Long
) {
var invoice by remember { mutableStateOf("") }
val clipboardManager = LocalClipboardManager.current
if (currentMeltQuote != null) {
// Show quote and pay button
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Quote info card
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Filled.FlashOn,
contentDescription = "Lightning",
tint = Color(0xFFFFB000),
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "LIGHTNING PAYMENT QUOTE",
color = Color(0xFFFFB000),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
Spacer(modifier = Modifier.height(24.dp))
// Quote details
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF2A2A2A))
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Amount:",
color = Color.Gray,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp
)
Text(
text = formatSats(currentMeltQuote.amount.toLong()),
color = Color.White,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
if (currentMeltQuote.feeReserve.toLong() > 0) {
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Fee:",
color = Color.Gray,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp
)
Text(
text = formatSats(currentMeltQuote.feeReserve.toLong()),
color = Color.White,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(8.dp))
HorizontalDivider(color = Color.Gray)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Total:",
color = Color.Gray,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
Text(
text = formatSats(currentMeltQuote.amount.toLong() + currentMeltQuote.feeReserve.toLong()),
color = Color.White,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
}
}
}
}
}
// Pay button
Button(
onClick = {
viewModel.payLightningInvoice(currentMeltQuote.id)
},
enabled = !isLoading,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851),
disabledContainerColor = Color(0xFF2A2A2A)
),
shape = RoundedCornerShape(16.dp)
) {
if (isLoading) {
CircularProgressIndicator(
color = Color.Black,
modifier = Modifier.size(24.dp)
)
} else {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.FlashOn,
contentDescription = "Pay Invoice",
tint = Color.Black,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "PAY INVOICE",
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
}
} else {
// Show invoice input
Column(
modifier = Modifier.fillMaxWidth()
) {
// Invoice input field
OutlinedTextField(
value = invoice,
onValueChange = { invoice = it },
label = {
Text(
text = "Lightning Invoice",
fontFamily = FontFamily.Monospace
)
},
placeholder = {
Text(
text = "lnbc...",
fontFamily = FontFamily.Monospace,
color = Color.Gray
)
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF00C851),
focusedLabelColor = Color(0xFF00C851),
unfocusedBorderColor = Color(0xFF2A2A2A),
unfocusedLabelColor = Color.Gray,
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
minLines = 3,
maxLines = 4,
shape = RoundedCornerShape(16.dp),
trailingIcon = {
IconButton(
onClick = {
// TODO: Implement QR code scanning
}
) {
Icon(
imageVector = Icons.Filled.QrCode,
contentDescription = "Scan QR",
tint = Color(0xFF00C851)
)
}
}
)
// Paste button
Button(
onClick = {
clipboardManager.getText()?.text?.let { clipText ->
if (clipText.startsWith("lnbc") || clipText.startsWith("lnbtb")) {
invoice = clipText
}
}
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.padding(bottom = 16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
border = androidx.compose.foundation.BorderStroke(
2.dp,
Color(0xFF00C851)
),
shape = RoundedCornerShape(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.ContentPaste,
contentDescription = "Paste",
tint = Color(0xFF00C851),
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "PASTE FROM CLIPBOARD",
color = Color(0xFF00C851),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
// Get quote button
Button(
onClick = {
if (invoice.isNotEmpty()) {
viewModel.createMeltQuote(invoice)
}
},
enabled = !isLoading && invoice.isNotEmpty(),
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851),
disabledContainerColor = Color(0xFF2A2A2A)
),
shape = RoundedCornerShape(16.dp)
) {
if (isLoading) {
CircularProgressIndicator(
color = Color.Black,
modifier = Modifier.size(24.dp)
)
} else {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.Assessment,
contentDescription = "Get Quote",
tint = Color.Black,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "GET QUOTE",
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
}
}
}
// 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"
}
}
@@ -0,0 +1,232 @@
package com.bitchat.android.wallet.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
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.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
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
/**
* Ecash (Cashu) token send dialog content
*/
@Composable
fun SendEcashDialog(
viewModel: WalletViewModel,
generatedToken: String?,
isLoading: Boolean,
maxAmount: Long
) {
var amount by remember { mutableStateOf("") }
var memo by remember { mutableStateOf("") }
val clipboardManager = LocalClipboardManager.current
if (generatedToken != null) {
// Show generated token
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Token info card
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Filled.Toll,
contentDescription = "Token",
tint = Color(0xFF00C851),
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "CASHU TOKEN CREATED",
color = Color(0xFF00C851),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
Spacer(modifier = Modifier.height(24.dp))
// Token display
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF2A2A2A))
) {
SelectionContainer {
Text(
text = generatedToken,
color = Color.White,
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
modifier = Modifier.padding(16.dp),
textAlign = TextAlign.Center
)
}
}
}
}
// Copy button
Button(
onClick = {
clipboardManager.setText(AnnotatedString(generatedToken))
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851)
),
shape = RoundedCornerShape(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.ContentCopy,
contentDescription = "Copy",
tint = Color.Black,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "COPY TOKEN",
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
} else {
// Show input form
Column(
modifier = Modifier.fillMaxWidth()
) {
// Amount input
OutlinedTextField(
value = amount,
onValueChange = { amount = it },
label = {
Text(
text = "Amount (sats)",
fontFamily = FontFamily.Monospace
)
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF00C851),
focusedLabelColor = Color(0xFF00C851),
unfocusedBorderColor = Color(0xFF2A2A2A),
unfocusedLabelColor = Color.Gray,
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
shape = RoundedCornerShape(16.dp)
)
// Memo input
OutlinedTextField(
value = memo,
onValueChange = { memo = it },
label = {
Text(
text = "Memo (optional)",
fontFamily = FontFamily.Monospace
)
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF00C851),
focusedLabelColor = Color(0xFF00C851),
unfocusedBorderColor = Color(0xFF2A2A2A),
unfocusedLabelColor = Color.Gray,
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp),
shape = RoundedCornerShape(16.dp)
)
// Create token button
Button(
onClick = {
amount.toLongOrNull()?.let { amountSats ->
viewModel.createCashuToken(amountSats, memo.ifEmpty { null })
}
},
enabled = !isLoading && amount.toLongOrNull()?.let { it > 0 && it <= maxAmount } == true,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851),
disabledContainerColor = Color(0xFF2A2A2A)
),
shape = RoundedCornerShape(16.dp)
) {
if (isLoading) {
CircularProgressIndicator(
color = Color.Black,
modifier = Modifier.size(24.dp)
)
} else {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.Toll,
contentDescription = "Create Token",
tint = Color.Black,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "CREATE TOKEN",
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
}
}
}
@@ -0,0 +1,333 @@
package com.bitchat.android.wallet.ui
import androidx.compose.foundation.BorderStroke
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.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
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 com.bitchat.android.wallet.data.MeltQuote
import com.bitchat.android.wallet.viewmodel.WalletViewModel
/**
* Lightning payment send dialog content
*/
@Composable
fun SendLightningDialog(
viewModel: WalletViewModel,
currentMeltQuote: MeltQuote?,
isLoading: Boolean,
maxAmount: Long
) {
var invoice by remember { mutableStateOf("") }
val clipboardManager = LocalClipboardManager.current
if (currentMeltQuote != null) {
// Show quote and pay button
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Quote info card
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Filled.FlashOn,
contentDescription = "Lightning",
tint = Color(0xFFFFB000),
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "LIGHTNING PAYMENT QUOTE",
color = Color(0xFFFFB000),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
Spacer(modifier = Modifier.height(24.dp))
// Quote details
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF2A2A2A))
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Amount:",
color = Color.Gray,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp
)
Text(
text = WalletUtils.formatSats(currentMeltQuote.amount.toLong()),
color = Color.White,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
if (currentMeltQuote.feeReserve.toLong() > 0) {
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Fee:",
color = Color.Gray,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp
)
Text(
text = WalletUtils.formatSats(currentMeltQuote.feeReserve.toLong()),
color = Color.White,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(8.dp))
HorizontalDivider(color = Color.Gray)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Total:",
color = Color.Gray,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
Text(
text = WalletUtils.formatSats(currentMeltQuote.amount.toLong() + currentMeltQuote.feeReserve.toLong()),
color = Color.White,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
}
}
}
}
}
// Pay button
Button(
onClick = {
viewModel.payLightningInvoice(currentMeltQuote.id)
},
enabled = !isLoading,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851),
disabledContainerColor = Color(0xFF2A2A2A)
),
shape = RoundedCornerShape(16.dp)
) {
if (isLoading) {
CircularProgressIndicator(
color = Color.Black,
modifier = Modifier.size(24.dp)
)
} else {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.FlashOn,
contentDescription = "Pay Invoice",
tint = Color.Black,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "PAY INVOICE",
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
}
} else {
// Show invoice input
Column(
modifier = Modifier.fillMaxWidth()
) {
// Invoice input field
OutlinedTextField(
value = invoice,
onValueChange = { invoice = it },
label = {
Text(
text = "Lightning Invoice",
fontFamily = FontFamily.Monospace
)
},
placeholder = {
Text(
text = "lnbc...",
fontFamily = FontFamily.Monospace,
color = Color.Gray
)
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF00C851),
focusedLabelColor = Color(0xFF00C851),
unfocusedBorderColor = Color(0xFF2A2A2A),
unfocusedLabelColor = Color.Gray,
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
minLines = 3,
maxLines = 4,
shape = RoundedCornerShape(16.dp),
trailingIcon = {
IconButton(
onClick = {
// TODO: Implement QR code scanning
}
) {
Icon(
imageVector = Icons.Filled.QrCode,
contentDescription = "Scan QR",
tint = Color(0xFF00C851)
)
}
}
)
// Paste button
Button(
onClick = {
clipboardManager.getText()?.text?.let { clipText ->
if (clipText.startsWith("lnbc") || clipText.startsWith("lnbtb")) {
invoice = clipText
}
}
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.padding(bottom = 16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
border = BorderStroke(
2.dp,
Color(0xFF00C851)
),
shape = RoundedCornerShape(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.ContentPaste,
contentDescription = "Paste",
tint = Color(0xFF00C851),
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "PASTE FROM CLIPBOARD",
color = Color(0xFF00C851),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
// Get quote button
Button(
onClick = {
if (invoice.isNotEmpty()) {
viewModel.createMeltQuote(invoice)
}
},
enabled = !isLoading && invoice.isNotEmpty(),
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851),
disabledContainerColor = Color(0xFF2A2A2A)
),
shape = RoundedCornerShape(16.dp)
) {
if (isLoading) {
CircularProgressIndicator(
color = Color.Black,
modifier = Modifier.size(24.dp)
)
} else {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.Assessment,
contentDescription = "Get Quote",
tint = Color.Black,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "GET QUOTE",
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp
)
}
}
}
}
}
}
@@ -0,0 +1,17 @@
package com.bitchat.android.wallet.ui
/**
* Utility functions for wallet UI components
*/
object WalletUtils {
/**
* Format satoshis for display
*/
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"
}
}
}