mint info screen, wip

This commit is contained in:
callebtc
2025-07-16 14:28:38 +02:00
parent 7fc623d65a
commit 198a8ba6c5
11 changed files with 1429 additions and 178 deletions
@@ -2,6 +2,14 @@ package com.bitchat.android.wallet.data
import java.util.Date
/**
* Represents contact information for a mint
*/
data class ContactInfo(
val method: String, // e.g., "email", "twitter", "nostr"
val info: String // e.g., "contact@example.com", "@username", "npub..."
)
/**
* Represents a Cashu mint information
*/
@@ -10,9 +18,9 @@ data class MintInfo(
val name: String,
val description: String? = null,
val descriptionLong: String? = null,
val contact: String? = null,
val contact: List<ContactInfo>? = null,
val version: String,
val nuts: Map<String, String>, // Simplified - was Map<String, Any>
val nuts: Map<String, Any>? = null, // NUT specifications supported by the mint
val motd: String? = null,
val icon: String? = null,
val time: Date? = null
@@ -21,7 +21,6 @@ class CashuService {
private var currentMintUrl: String? = null
private var currentUnit: String = "sat"
private var isInitialized = false
private var isCdkAvailable = false
private var repository: WalletRepository? = null
companion object {
@@ -160,38 +159,6 @@ class CashuService {
}
}
/**
* Check if CDK is available and initialize appropriately
*/
private fun initializeCdkAvailability(): Boolean {
if (isCdkAvailable) return true
Log.d(TAG, "=== CDK LIBRARY AVAILABILITY CHECK ===")
try {
// Test CDK availability by calling a simple function
val testMnemonic = generateMnemonic()
Log.d(TAG, "✅ CDK library is fully available and functional!")
Log.d(TAG, "Generated test mnemonic with ${testMnemonic.split(" ").size} words")
isCdkAvailable = true
return true
} catch (e: UnsatisfiedLinkError) {
Log.w(TAG, "❌ CDK library not available: ${e.message}")
} catch (e: FfiException) {
Log.w(TAG, "❌ CDK FFI error: ${e.message}")
} catch (e: NoClassDefFoundError) {
Log.w(TAG, "❌ CDK classes not found: ${e.message}")
} catch (e: Exception) {
Log.w(TAG, "❌ Unexpected CDK error: ${e.message}")
}
isCdkAvailable = false
Log.w(TAG, "⚠️ CDK library not available - wallet will not function properly")
Log.d(TAG, "=== END CDK LIBRARY CHECK ===")
return false
}
/**
* Initialize the CDK wallet with a mint URL
*/
@@ -200,11 +167,6 @@ class CashuService {
try {
Log.d(TAG, "Initializing wallet with mint: $mintUrl")
// Test CDK availability first
if (!initializeCdkAvailability()) {
return@withContext Result.failure(Exception("CDK library not available"))
}
// Create database path using application context
val context = getApplicationContext()
val dbPath = "${context.filesDir}/cashu_wallet.db"
@@ -269,7 +231,7 @@ class CashuService {
initializeWallet(mintUrl).getOrThrow()
}
val mintInfo = if (isCdkAvailable && wallet != null) {
val mintInfo = if (wallet != null) {
try {
// Get real mint info from CDK
val mintInfoData = wallet!!.getMintInfo()
@@ -280,7 +242,7 @@ class CashuService {
name = extractMintName(mintUrl),
description = "Cashu mint via CDK",
descriptionLong = "Real Cashu mint connection via CDK-FFI",
contact = "",
contact = emptyList(),
version = "1.0.0",
nuts = mapOf(
"4" to "true", // NUT-4: Mint quote
@@ -318,7 +280,7 @@ class CashuService {
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
}
if (!isCdkAvailable || wallet == null) {
if (wallet == null) {
return@withContext Result.failure(Exception("CDK not available"))
}
@@ -349,7 +311,7 @@ class CashuService {
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
}
if (!isCdkAvailable || wallet == null) {
if (wallet == null) {
return@withContext Result.failure(Exception("CDK not available"))
}
@@ -446,7 +408,7 @@ class CashuService {
)
// Now receive the token with the properly initialized wallet
if (!isCdkAvailable || wallet == null) {
if (wallet == null) {
return@withContext Result.failure(Exception("CDK not available"))
}
@@ -476,7 +438,7 @@ class CashuService {
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
}
if (!isCdkAvailable || wallet == null) {
if (wallet == null) {
return@withContext Result.failure(Exception("CDK not available"))
}
@@ -527,7 +489,7 @@ class CashuService {
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
}
if (!isCdkAvailable || wallet == null) {
if (wallet == null) {
return@withContext Result.failure(Exception("CDK not available"))
}
@@ -574,7 +536,7 @@ class CashuService {
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
}
if (!isCdkAvailable || wallet == null) {
if (wallet == null) {
return@withContext Result.failure(Exception("CDK not available"))
}
@@ -619,7 +581,7 @@ class CashuService {
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
}
if (!isCdkAvailable || wallet == null) {
if (wallet == null) {
return@withContext Result.failure(Exception("CDK not available"))
}
@@ -693,14 +655,6 @@ class CashuService {
}
}
/**
* Check if real CDK is available
*/
fun isCdkAvailable(): Boolean {
initializeCdkAvailability()
return isCdkAvailable
}
// Helper functions
private fun getApplicationContext(): Context {
@@ -25,6 +25,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.bitchat.android.wallet.data.Mint
import com.bitchat.android.wallet.viewmodel.WalletViewModel
import com.bitchat.android.wallet.ui.mintinfo.MintDetailsScreen
import java.text.SimpleDateFormat
import java.util.*
@@ -41,7 +42,41 @@ fun MintsScreen(
val showAddMintDialog by viewModel.showAddMintDialog.observeAsState(false)
val isLoading by viewModel.isLoading.observeAsState(false)
val errorMessage by viewModel.errorMessage.observeAsState()
val showMintDetails by viewModel.showMintDetails.observeAsState(false)
val selectedMintUrl by viewModel.selectedMintUrl.observeAsState()
// Show mint details screen if selected
if (showMintDetails && selectedMintUrl != null) {
MintDetailsScreen(
mintUrl = selectedMintUrl!!,
walletViewModel = viewModel,
onNavigateBack = { viewModel.hideMintDetails() },
modifier = modifier
)
} else {
// Show normal mints list
MintsListContent(
viewModel = viewModel,
mints = mints,
activeMint = activeMint,
showAddMintDialog = showAddMintDialog,
isLoading = isLoading,
errorMessage = errorMessage,
modifier = modifier
)
}
}
@Composable
private fun MintsListContent(
viewModel: WalletViewModel,
mints: List<Mint>,
activeMint: String?,
showAddMintDialog: Boolean,
isLoading: Boolean,
errorMessage: String?,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
@@ -89,14 +124,14 @@ fun MintsScreen(
if (mints.isEmpty()) {
EmptyMintsCard(onAddClick = { viewModel.showAddMintDialog() })
} else {
MintsList(
mints = mints,
activeMint = activeMint,
onMintSelect = { viewModel.setActiveMint(it) },
onMintEdit = { mint, newNickname ->
viewModel.updateMintNickname(mint, newNickname)
}
)
MintsList(
mints = mints,
activeMint = activeMint,
onMintSelect = { viewModel.setActiveMint(it) },
onMintInfo = { mintUrl ->
viewModel.showMintDetails(mintUrl)
}
)
}
// Error message
@@ -123,7 +158,7 @@ private fun MintsList(
mints: List<Mint>,
activeMint: String?,
onMintSelect: (String) -> Unit,
onMintEdit: (String, String) -> Unit
onMintInfo: (String) -> Unit
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
@@ -133,7 +168,7 @@ private fun MintsList(
mint = mint,
isActive = mint.url == activeMint,
onSelect = { onMintSelect(mint.url) },
onEdit = { newNickname -> onMintEdit(mint.url, newNickname) }
onInfo = { onMintInfo(mint.url) }
)
}
}
@@ -144,9 +179,8 @@ private fun MintItem(
mint: Mint,
isActive: Boolean,
onSelect: () -> Unit,
onEdit: (String) -> Unit
onInfo: () -> Unit
) {
var showEditDialog by remember { mutableStateOf(false) }
Card(
modifier = Modifier
@@ -210,11 +244,11 @@ private fun MintItem(
)
}
// Edit button
IconButton(onClick = { showEditDialog = true }) {
// Info button
IconButton(onClick = onInfo) {
Icon(
imageVector = Icons.Filled.Edit,
contentDescription = "Edit",
imageVector = Icons.Filled.Info,
contentDescription = "Mint Details",
tint = Color.Gray,
modifier = Modifier.size(16.dp)
)
@@ -232,17 +266,7 @@ private fun MintItem(
}
}
// Edit dialog
if (showEditDialog) {
EditMintDialog(
currentNickname = mint.nickname,
onSave = {
onEdit(it)
showEditDialog = false
},
onDismiss = { showEditDialog = false }
)
}
}
@Composable
@@ -436,100 +460,7 @@ private fun AddMintDialog(
}
}
@Composable
private fun EditMintDialog(
currentNickname: String,
onSave: (String) -> Unit,
onDismiss: () -> Unit
) {
var nickname by remember { mutableStateOf(currentNickname) }
Dialog(onDismissRequest = onDismiss) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
) {
Column(
modifier = Modifier.padding(24.dp)
) {
Text(
text = "Edit Mint Nickname",
color = Color.White,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = nickname,
onValueChange = { nickname = it },
label = {
Text(
text = "Nickname",
fontFamily = FontFamily.Monospace
)
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF00C851),
focusedLabelColor = Color(0xFF00C851),
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Cancel button
Button(
onClick = onDismiss,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
border = androidx.compose.foundation.BorderStroke(
1.dp,
Color.Gray
),
shape = RoundedCornerShape(8.dp)
) {
Text(
text = "Cancel",
color = Color.Gray,
fontFamily = FontFamily.Monospace
)
}
// Save button
Button(
onClick = { onSave(nickname) },
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851)
),
shape = RoundedCornerShape(8.dp),
enabled = nickname.isNotEmpty()
) {
Text(
text = "Save",
color = Color.Black,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
}
}
}
}
}
}
@Composable
private fun ErrorCard(
@@ -0,0 +1,139 @@
package com.bitchat.android.wallet.ui.mintinfo
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.graphics.vector.ImageVector
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.unit.dp
import androidx.compose.ui.unit.sp
import com.bitchat.android.wallet.data.Mint
/**
* Action buttons section for mint operations
*/
@Composable
fun MintActionButtons(
mint: Mint,
onEditNickname: () -> Unit,
onCopyUrl: () -> Unit,
onDeleteMint: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Section divider and title
SectionDivider(title = "ACTIONS")
// Action buttons container
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Edit nickname action
ActionButton(
icon = Icons.Filled.Edit,
label = "Edit Nickname",
onClick = onEditNickname
)
// Copy URL action
ActionButton(
icon = Icons.Filled.ContentCopy,
label = "Copy Mint URL",
onClick = onCopyUrl
)
// Delete mint action
ActionButton(
icon = Icons.Filled.Delete,
label = "Delete Mint",
onClick = onDeleteMint,
isDestructive = true
)
}
}
}
@Composable
private fun ActionButton(
icon: ImageVector,
label: String,
onClick: () -> Unit,
isDestructive: Boolean = false,
modifier: Modifier = Modifier
) {
val iconColor = if (isDestructive) Color(0xFFFF453A) else Color.Gray
val textColor = if (isDestructive) Color(0xFFFF453A) else Color.White
Row(
modifier = modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = label,
tint = iconColor,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = label,
color = textColor,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
fontFamily = FontFamily.Monospace
)
}
}
@Composable
private fun SectionDivider(
title: String,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.weight(1f)
.height(1.dp)
.background(Color(0xFF333333))
)
Text(
text = title,
color = Color.White,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
fontFamily = FontFamily.Monospace,
modifier = Modifier.padding(horizontal = 10.dp)
)
Box(
modifier = Modifier
.weight(1f)
.height(1.dp)
.background(Color(0xFF333333))
)
}
}
@@ -0,0 +1,301 @@
package com.bitchat.android.wallet.ui.mintinfo
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.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.bitchat.android.wallet.data.Mint
/**
* Dialog for editing mint nickname
*/
@Composable
fun EditMintNicknameDialog(
currentNickname: String,
onSave: (String) -> Unit,
onDismiss: () -> Unit
) {
var nickname by remember { mutableStateOf(currentNickname) }
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
) {
Column(
modifier = Modifier.padding(24.dp)
) {
// Header
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Edit Mint Nickname",
color = Color.White,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
IconButton(onClick = onDismiss) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Close",
tint = Color.Gray
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Nickname input
OutlinedTextField(
value = nickname,
onValueChange = { nickname = it },
label = {
Text(
text = "Nickname",
fontFamily = FontFamily.Monospace
)
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF00C851),
focusedLabelColor = Color(0xFF00C851),
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
// Action buttons
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Cancel button
OutlinedButton(
onClick = onDismiss,
modifier = Modifier.weight(1f),
border = androidx.compose.foundation.BorderStroke(
1.dp,
Color.Gray
),
shape = RoundedCornerShape(8.dp)
) {
Text(
text = "Cancel",
color = Color.Gray,
fontFamily = FontFamily.Monospace
)
}
// Save button
Button(
onClick = { onSave(nickname) },
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851)
),
shape = RoundedCornerShape(8.dp),
enabled = nickname.isNotEmpty()
) {
Text(
text = "Save",
color = Color.Black,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
}
}
}
}
}
}
/**
* Dialog for confirming mint deletion
*/
@Composable
fun DeleteMintDialog(
mint: Mint,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
) {
Column(
modifier = Modifier.padding(24.dp)
) {
// Header
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Delete Mint",
color = Color(0xFFFF453A),
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
IconButton(onClick = onDismiss) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Close",
tint = Color.Gray
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Warning message
Text(
text = "Are you sure you want to delete this mint from your wallet?",
color = Color.White,
fontSize = 16.sp,
fontFamily = FontFamily.Monospace,
lineHeight = 24.sp
)
Spacer(modifier = Modifier.height(16.dp))
// Mint details
Card(
colors = CardDefaults.cardColors(
containerColor = Color(0x10FFFFFF)
),
shape = RoundedCornerShape(8.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Nickname
MintDetailRow(
label = "Nickname:",
value = mint.nickname
)
// URL
MintDetailRow(
label = "URL:",
value = mint.url
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Warning note
Text(
text = "⚠️ This action cannot be undone. Any tokens from this mint will need to be re-added manually.",
color = Color(0xFFF18408),
fontSize = 14.sp,
fontFamily = FontFamily.Monospace,
lineHeight = 20.sp
)
Spacer(modifier = Modifier.height(24.dp))
// Action buttons
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Cancel button
OutlinedButton(
onClick = onDismiss,
modifier = Modifier.weight(1f),
border = androidx.compose.foundation.BorderStroke(
1.dp,
Color.Gray
),
shape = RoundedCornerShape(8.dp)
) {
Text(
text = "Cancel",
color = Color.Gray,
fontFamily = FontFamily.Monospace
)
}
// Delete button
Button(
onClick = onConfirm,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFFF453A)
),
shape = RoundedCornerShape(8.dp)
) {
Text(
text = "Delete",
color = Color.White,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
}
}
}
}
}
}
@Composable
private fun MintDetailRow(
label: String,
value: String,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.Top
) {
Text(
text = label,
color = Color.Gray,
fontSize = 14.sp,
fontFamily = FontFamily.Monospace,
modifier = Modifier.width(80.dp)
)
Text(
text = value,
color = Color.White,
fontSize = 14.sp,
fontFamily = FontFamily.Monospace,
modifier = Modifier.weight(1f)
)
}
}
@@ -0,0 +1,267 @@
package com.bitchat.android.wallet.ui.mintinfo
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.graphics.Color
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.Mint
import com.bitchat.android.wallet.viewmodel.WalletViewModel
/**
* Detailed mint information screen with terminal-like design
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MintDetailsScreen(
mintUrl: String,
walletViewModel: WalletViewModel,
onNavigateBack: () -> Unit,
modifier: Modifier = Modifier
) {
val mints by walletViewModel.mints.observeAsState(emptyList())
val isLoading by walletViewModel.isLoading.observeAsState(false)
val errorMessage by walletViewModel.errorMessage.observeAsState()
// Find the mint by URL
val mint = mints.find { it.url == mintUrl }
// Show error if mint not found
if (mint == null) {
MintNotFoundScreen(onNavigateBack = onNavigateBack)
return
}
// State for MOTD dismissal
var isMotdDismissed by remember { mutableStateOf(false) }
// State for dialogs
var showEditDialog by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) }
Column(
modifier = modifier
.fillMaxSize()
.background(Color.Black)
) {
// Header with back button
TopAppBar(
title = {
Text(
text = "Mint Details",
color = Color(0xFF00C851),
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
)
},
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back",
tint = Color(0xFF00C851)
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Black
)
)
// Scrollable content
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Mint Header Section
MintHeaderSection(mint = mint)
// MOTD Section
if (mint.info?.motd?.isNotEmpty() == true) {
MintMotdComponent(
message = mint.info.motd,
isDismissed = isMotdDismissed,
onDismiss = { isMotdDismissed = true }
)
}
// Description Section
MintDescriptionSection(mint = mint)
// Contact Section
if (mint.info?.contact?.isNotEmpty() == true) {
MintContactSection(contacts = mint.info.contact)
}
// Details Section
MintDetailsSection(mint = mint)
// Actions Section
MintActionButtons(
mint = mint,
onEditNickname = { showEditDialog = true },
onCopyUrl = { walletViewModel.copyToClipboard(mint.url) },
onDeleteMint = { showDeleteDialog = true }
)
// Error display
errorMessage?.let { error ->
ErrorCard(
message = error,
onDismiss = { walletViewModel.clearError() }
)
}
// Bottom spacing
Spacer(modifier = Modifier.height(32.dp))
}
}
// Edit nickname dialog
if (showEditDialog) {
EditMintNicknameDialog(
currentNickname = mint.nickname,
onSave = { newNickname ->
walletViewModel.updateMintNickname(mint.url, newNickname)
showEditDialog = false
},
onDismiss = { showEditDialog = false }
)
}
// Delete mint dialog
if (showDeleteDialog) {
DeleteMintDialog(
mint = mint,
onConfirm = {
walletViewModel.deleteMint(mint.url)
showDeleteDialog = false
onNavigateBack()
},
onDismiss = { showDeleteDialog = false }
)
}
// Loading overlay
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
color = Color(0xFF00C851)
)
}
}
}
@Composable
private fun MintNotFoundScreen(
onNavigateBack: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = "Error",
tint = Color.Red,
modifier = Modifier.size(64.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Mint Not Found",
color = Color.White,
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
Text(
text = "The requested mint could not be found",
color = Color.Gray,
fontSize = 16.sp,
fontFamily = FontFamily.Monospace
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = onNavigateBack,
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851)
)
) {
Text(
text = "Go Back",
color = Color.Black,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
}
}
}
@Composable
private fun ErrorCard(
message: String,
onDismiss: () -> Unit
) {
Card(
colors = CardDefaults.cardColors(
containerColor = Color(0x30FF0000)
)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = "Error",
tint = Color.Red,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = message,
color = Color.Red,
fontSize = 14.sp,
fontFamily = FontFamily.Monospace,
modifier = Modifier.weight(1f)
)
IconButton(onClick = onDismiss) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Close",
tint = Color.Red,
modifier = Modifier.size(16.dp)
)
}
}
}
}
@@ -0,0 +1,436 @@
package com.bitchat.android.wallet.ui.mintinfo
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.graphics.vector.ImageVector
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.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.bitchat.android.wallet.data.ContactInfo
import com.bitchat.android.wallet.data.Mint
import java.text.SimpleDateFormat
import java.util.*
/**
* Header section showing mint icon and name
*/
@Composable
fun MintHeaderSection(
mint: Mint,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1A1A1A).copy(alpha = 0.05f) // Very subtle background
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Mint icon/avatar
Card(
modifier = Modifier.size(56.dp),
shape = RoundedCornerShape(28.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2A2A2A)
)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
// For now, we'll use the AccountBalance icon as default
// In the future, could load from mint.info?.iconUrl
Icon(
imageVector = Icons.Filled.AccountBalance,
contentDescription = "Mint Icon",
tint = Color(0xFF00C851),
modifier = Modifier.size(32.dp)
)
}
}
Spacer(modifier = Modifier.height(12.dp))
// Mint name from info or fallback to nickname
Text(
text = mint.info?.name?.takeIf { it.isNotEmpty() } ?: mint.nickname,
color = Color.White,
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
// Show nickname separately if different from name
if (mint.info?.name?.isNotEmpty() == true && mint.info.name != mint.nickname) {
Text(
text = "\"${mint.nickname}\"",
color = Color.Gray,
fontSize = 14.sp,
fontFamily = FontFamily.Monospace
)
}
}
}
}
/**
* Description section showing mint description and long description
*/
@Composable
fun MintDescriptionSection(
mint: Mint,
modifier: Modifier = Modifier
) {
val info = mint.info
if (info?.description.isNullOrEmpty() && info?.descriptionLong.isNullOrEmpty()) {
return
}
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Short description
info?.description?.takeIf { it.isNotEmpty() }?.let { description ->
Text(
text = description,
color = Color.White,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
lineHeight = 24.sp,
fontFamily = FontFamily.Monospace
)
}
// Long description
info?.descriptionLong?.takeIf { it.isNotEmpty() }?.let { longDescription ->
Text(
text = longDescription,
color = Color.Gray,
fontSize = 14.sp,
lineHeight = 20.sp,
fontFamily = FontFamily.Monospace
)
}
}
}
/**
* Contact section showing mint contact information
*/
@Composable
fun MintContactSection(
contacts: List<ContactInfo>,
modifier: Modifier = Modifier
) {
if (contacts.isEmpty()) return
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Section divider and title
SectionDivider(title = "CONTACT")
// Contact items
contacts.forEach { contact ->
ContactItem(contact = contact)
}
}
}
/**
* Details section showing mint technical information
*/
@Composable
fun MintDetailsSection(
mint: Mint,
modifier: Modifier = Modifier
) {
var showAllNuts by remember { mutableStateOf(false) }
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Section divider and title
SectionDivider(title = "DETAILS")
// URL
DetailItem(
icon = Icons.Filled.Link,
label = "URL",
value = mint.url,
onClick = { /* Copy URL handled by parent */ }
)
// NUTs (Cashu specifications)
mint.info?.nuts?.let { nuts ->
val supportedNuts = nuts.filter { (_, nutInfo) ->
when (nutInfo) {
is Map<*, *> -> nutInfo["supported"] == true || nutInfo["disabled"] != true
else -> true
}
}
if (supportedNuts.isNotEmpty()) {
DetailItem(
icon = Icons.Filled.Extension,
label = "NUTs",
value = if (showAllNuts) "Hide" else "Show all",
onClick = { showAllNuts = !showAllNuts }
)
if (showAllNuts) {
NutsExpandedSection(nuts = supportedNuts)
}
}
}
// Version
mint.info?.version?.takeIf { it.isNotEmpty() }?.let { version ->
DetailItem(
icon = Icons.Filled.Info,
label = "Version",
value = version
)
}
// Date added
DetailItem(
icon = Icons.Filled.CalendarToday,
label = "Added",
value = formatDate(mint.dateAdded)
)
}
}
@Composable
private fun ContactItem(
contact: ContactInfo,
modifier: Modifier = Modifier
) {
val clipboardManager = LocalClipboardManager.current
Row(
modifier = modifier
.fillMaxWidth()
.clickable {
clipboardManager.setText(AnnotatedString(contact.info))
}
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Contact method icon
val icon = when (contact.method.lowercase()) {
"email" -> Icons.Filled.Email
"twitter", "x" -> Icons.Filled.Share
"nostr" -> Icons.Filled.Public
else -> Icons.Filled.ContactMail
}
Icon(
imageVector = icon,
contentDescription = contact.method,
tint = Color.Gray,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = contact.info,
color = Color.White,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
fontFamily = FontFamily.Monospace,
modifier = Modifier.weight(1f),
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
Icon(
imageVector = Icons.Filled.ContentCopy,
contentDescription = "Copy",
tint = Color.Gray,
modifier = Modifier.size(20.dp)
)
}
}
@Composable
private fun DetailItem(
icon: ImageVector,
label: String,
value: String,
onClick: (() -> Unit)? = null,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.then(
if (onClick != null) Modifier.clickable { onClick() }
else Modifier
)
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = label,
tint = Color.Gray,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = label,
color = Color.Gray,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
fontFamily = FontFamily.Monospace
)
Spacer(modifier = Modifier.weight(1f))
Text(
text = value,
color = Color.White,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
fontFamily = FontFamily.Monospace,
textAlign = androidx.compose.ui.text.style.TextAlign.Right,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(0.6f)
)
}
}
@Composable
private fun NutsExpandedSection(
nuts: Map<String, Any>,
modifier: Modifier = Modifier
) {
val nutNames = mapOf(
"7" to "Token state check",
"8" to "Overpaid Lightning fees",
"9" to "Signature restore",
"10" to "Spending conditions",
"11" to "Pay-To-Pubkey (P2PK)",
"12" to "DLEQ proofs",
"13" to "Deterministic secrets",
"14" to "Hashed Timelock Contracts",
"15" to "Partial multi-path payments",
"16" to "Animated QR codes",
"17" to "WebSocket subscriptions",
"18" to "Payment requests",
"19" to "Cached Responses",
"20" to "Signature on Mint Quote",
"21" to "Clear authentication",
"22" to "Blind authentication"
)
Column(
modifier = modifier
.fillMaxWidth()
.padding(start = 32.dp), // Indent under the main detail item
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
nuts.filter { (nutNumber, _) ->
nutNumber.toIntOrNull()?.let { it >= 7 } == true // Only show NUT-7 and above
}.forEach { (nutNumber, _) ->
val nutName = nutNames[nutNumber] ?: "Unknown NUT"
Card(
shape = RoundedCornerShape(4.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2A2A2A)
)
) {
Row(
modifier = Modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "$nutNumber:",
color = Color.Gray,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
fontFamily = FontFamily.Monospace
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = nutName,
color = Color.White,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
fontFamily = FontFamily.Monospace
)
}
}
}
}
}
@Composable
private fun SectionDivider(
title: String,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.weight(1f)
.height(1.dp)
.background(Color(0xFF333333))
)
Text(
text = title,
color = Color.White,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
fontFamily = FontFamily.Monospace,
modifier = Modifier.padding(horizontal = 10.dp)
)
Box(
modifier = Modifier
.weight(1f)
.height(1.dp)
.background(Color(0xFF333333))
)
}
}
// Utility function
private fun formatDate(date: Date): String {
return SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()).format(date)
}
@@ -0,0 +1,146 @@
package com.bitchat.android.wallet.ui.mintinfo
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
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.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
* Component for displaying mint MOTD (Message of the Day) with dismiss functionality
*/
@Composable
fun MintMotdComponent(
message: String,
isDismissed: Boolean,
onDismiss: () -> Unit,
modifier: Modifier = Modifier
) {
if (message.isEmpty()) return
if (!isDismissed) {
// Active MOTD card
Card(
modifier = modifier
.fillMaxWidth()
.clickable(enabled = false) { }, // Prevent accidental clicks
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(
containerColor = Color.Black
),
border = BorderStroke(1.dp, Color(0xFFF18408)) // Orange border like in Vue version
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.Top
) {
// Info icon
Icon(
imageVector = Icons.Filled.Info,
contentDescription = "MOTD",
tint = Color(0xFFF18408),
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
// Content
Column(
modifier = Modifier.weight(1f)
) {
// Header with title and close button
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Message of the Day",
color = Color(0xFFF18408),
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
fontFamily = FontFamily.Monospace
)
IconButton(
onClick = onDismiss,
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Dismiss",
tint = Color(0xFFF18408),
modifier = Modifier.size(18.dp)
)
}
}
Spacer(modifier = Modifier.height(2.dp))
// MOTD message
Text(
text = message,
color = Color(0xFFF18408),
fontSize = 14.sp,
lineHeight = 16.sp,
fontFamily = FontFamily.Monospace
)
}
}
}
} else {
// Dismissed MOTD - minimal display
Card(
modifier = modifier
.fillMaxWidth()
.clickable { /* Could expand back if needed */ },
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1A1A1A)
)
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.Info,
contentDescription = "MOTD",
tint = Color.Gray,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
text = "Message of the Day",
color = Color.Gray,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
fontFamily = FontFamily.Monospace
)
Text(
text = message,
color = Color.Gray,
fontSize = 14.sp,
lineHeight = 16.sp,
fontFamily = FontFamily.Monospace,
maxLines = 2 // Truncate in dismissed state
)
}
}
}
}
}
@@ -271,6 +271,43 @@ class MintManager(
}
}
/**
* Delete a mint from the repository
*/
fun deleteMint(mintUrl: String) {
coroutineScope.launch {
try {
val currentMints = _mints.value ?: emptyList()
val updatedMints = currentMints.filter { it.url != mintUrl }
// Save updated mints list (remove the mint)
repository.clearAllData()
updatedMints.forEach { mint ->
repository.saveMint(mint).onFailure { error ->
Log.e(TAG, "Failed to save mint during deletion: ${mint.url}", error)
}
}
// If the deleted mint was active, clear active mint
if (_activeMint.value == mintUrl) {
repository.setActiveMint("")
_activeMint.value = null
}
// Reload mints
repository.getMints().onSuccess { mintList ->
_mints.value = mintList
}
Log.d(TAG, "Successfully deleted mint: $mintUrl")
} catch (e: Exception) {
Log.e(TAG, "Failed to delete mint: $mintUrl", e)
_errorMessage.value = "Failed to delete mint: ${e.message}"
}
}
}
/**
* Clear error message
*/
@@ -24,6 +24,12 @@ class UIStateManager {
private val _showAddMintDialog = MutableLiveData<Boolean>(false)
val showAddMintDialog: androidx.lifecycle.LiveData<Boolean> = _showAddMintDialog
private val _showMintDetails = MutableLiveData<Boolean>(false)
val showMintDetails: androidx.lifecycle.LiveData<Boolean> = _showMintDetails
private val _selectedMintUrl = MutableLiveData<String?>(null)
val selectedMintUrl: androidx.lifecycle.LiveData<String?> = _selectedMintUrl
// Success animation state
private val _showSuccessAnimation = MutableLiveData<Boolean>(false)
val showSuccessAnimation: androidx.lifecycle.LiveData<Boolean> = _showSuccessAnimation
@@ -84,6 +90,16 @@ class UIStateManager {
_showAddMintDialog.value = false
}
fun showMintDetails(mintUrl: String) {
_selectedMintUrl.value = mintUrl
_showMintDetails.value = true
}
fun hideMintDetails() {
_showMintDetails.value = false
_selectedMintUrl.value = null
}
// Animation Management
fun showSuccessAnimation(animationData: WalletViewModel.SuccessAnimationData) {
@@ -58,6 +58,8 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
val sendType: LiveData<SendType> = uiStateManager.sendType
val receiveType: LiveData<ReceiveType> = uiStateManager.receiveType
val showAddMintDialog: LiveData<Boolean> = uiStateManager.showAddMintDialog
val showMintDetails: LiveData<Boolean> = uiStateManager.showMintDetails
val selectedMintUrl: LiveData<String?> = uiStateManager.selectedMintUrl
val showSuccessAnimation: LiveData<Boolean> = uiStateManager.showSuccessAnimation
val successAnimationData: LiveData<SuccessAnimationData?> = uiStateManager.successAnimationData
val showFailureAnimation: LiveData<Boolean> = uiStateManager.showFailureAnimation
@@ -182,6 +184,20 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
fun clearError() = uiStateManager.clearError()
// Mint details navigation
fun showMintDetails(mintUrl: String) = uiStateManager.showMintDetails(mintUrl)
fun hideMintDetails() = uiStateManager.hideMintDetails()
// Copy to clipboard functionality
fun copyToClipboard(text: String) {
// This will be handled by the UI components directly using LocalClipboardManager
// We can add a success message here if needed
}
// Delete mint functionality
fun deleteMint(mintUrl: String) = mintManager.deleteMint(mintUrl)
// Token input management - Delegate to TokenManager
fun setTokenInput(token: String) = tokenManager.setTokenInput(token)