feat: improve nwc qr code ux
This commit is contained in:
@@ -330,7 +330,7 @@ data class NwcKey(
|
||||
val wallet: String,
|
||||
val description: String,
|
||||
val expires_at: Long,
|
||||
val permissions: String, // comma-separated permission names
|
||||
val permissions: String,
|
||||
val created_at: Long,
|
||||
val last_used: Long
|
||||
)
|
||||
@@ -352,7 +352,8 @@ data class NwcGetResponse(
|
||||
|
||||
data class NwcNewBudget(
|
||||
val budget_msats: Long,
|
||||
val refresh_window: Int // seconds
|
||||
val refresh_window: Int, // seconds
|
||||
val created_at : Long = System.currentTimeMillis() / 1000L
|
||||
)
|
||||
|
||||
/** Request body for PUT /nwc/{pubkey} */
|
||||
|
||||
@@ -105,6 +105,8 @@ val EnHomeStrings = AppStrings(
|
||||
qrCopy = "Copy",
|
||||
qrNfcStop = "Stop NFC sharing",
|
||||
qrNfcStart = "Share via NFC",
|
||||
openInApp = "Open in app",
|
||||
noAppFound = "No compatible app found",
|
||||
|
||||
// ── ReceiveViewModel ──────────────────────────────────────────────────────
|
||||
receiveVmFailedToCreateInvoice = "Failed to create invoice",
|
||||
|
||||
@@ -105,6 +105,8 @@ val EsHomeStrings = AppStrings(
|
||||
qrCopy = "Copiar",
|
||||
qrNfcStop = "Detener NFC",
|
||||
qrNfcStart = "Compartir por NFC",
|
||||
openInApp = "Abrir en app",
|
||||
noAppFound = "No hay aplicación compatible",
|
||||
|
||||
// ── ReceiveViewModel ──────────────────────────────────────────────────────
|
||||
receiveVmFailedToCreateInvoice = "No se pudo crear la factura",
|
||||
|
||||
@@ -104,6 +104,8 @@ val EuStrings = AppStrings(
|
||||
qrCopy = "Kopiatu",
|
||||
qrNfcStop = "Gelditu NFC partekatzea",
|
||||
qrNfcStart = "Partekatu NFC bidez",
|
||||
openInApp = "Ireki aplikazioan",
|
||||
noAppFound = "Ez da aurkitu aplikazio bateragarririk",
|
||||
|
||||
// ── ReceiveViewModel ──────────────────────────────────────────────────────
|
||||
receiveVmFailedToCreateInvoice = "Ezin izan da faktura sortu",
|
||||
|
||||
@@ -113,6 +113,8 @@ data class AppStrings(
|
||||
val qrCopy : String,
|
||||
val qrNfcStop : String,
|
||||
val qrNfcStart : String,
|
||||
val openInApp : String,
|
||||
val noAppFound : String,
|
||||
|
||||
// ── ReceiveViewModel ──────────────────────────────────────────────────────
|
||||
val receiveVmFailedToCreateInvoice : String,
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package com.bitcointxoko.gudariwallet.ui.nwc
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
@@ -14,7 +19,7 @@ import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Cable
|
||||
import androidx.compose.material.icons.filled.CalendarToday
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material3.*
|
||||
@@ -22,13 +27,14 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitcointxoko.gudariwallet.api.NwcGetResponse
|
||||
import com.bitcointxoko.gudariwallet.api.NwcNewBudget
|
||||
import com.bitcointxoko.gudariwallet.ui.receive.QrDisplayCard
|
||||
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.Instant
|
||||
@@ -37,6 +43,7 @@ import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import androidx.core.net.toUri
|
||||
|
||||
// ── Available permissions ─────────────────────────────────────────────────────
|
||||
internal data class NwcPermission(
|
||||
@@ -53,6 +60,36 @@ internal val ALL_PERMISSIONS = listOf(
|
||||
NwcPermission("info", "Read account info", default = false)
|
||||
)
|
||||
|
||||
// ── Budget ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Transient UI state for a single budget row in the "Add connection" sheet.
|
||||
* Converted to [NwcNewBudget] on submit.
|
||||
*/
|
||||
enum class BudgetRefreshWindow(val label: String, val seconds: Int) {
|
||||
DAILY ("Daily", 86_400),
|
||||
WEEKLY ("Weekly", 604_800),
|
||||
MONTHLY ("Monthly", 2_592_000),
|
||||
YEARLY ("Yearly", 31_536_000),
|
||||
NEVER ("Never", 0);
|
||||
}
|
||||
|
||||
data class BudgetDraft(
|
||||
val id : Int = 0, // stable key for LazyColumn / forEach
|
||||
val amountSats : String = "", // raw text field value (sats)
|
||||
val refreshWindow : BudgetRefreshWindow = BudgetRefreshWindow.MONTHLY
|
||||
) {
|
||||
/** Returns null when the amount field is blank or non-numeric. */
|
||||
fun toNwcNewBudget(): NwcNewBudget? {
|
||||
val sats = amountSats.trim().toLongOrNull()?.takeIf { it > 0 } ?: return null
|
||||
return NwcNewBudget(
|
||||
budget_msats = sats * 1_000L,
|
||||
refresh_window = refreshWindow.seconds
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Screen ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -149,11 +186,12 @@ fun NwcScreen(
|
||||
if (showAddSheet) {
|
||||
NwcAddConnectionSheet(
|
||||
isCreating = isCreating,
|
||||
onCreate = { description, permissions, expiresAt ->
|
||||
onCreate = { description, permissions, expiresAt, budgets ->
|
||||
viewModel.createConnection(
|
||||
description = description,
|
||||
permissions = permissions,
|
||||
expiresAt = expiresAt
|
||||
expiresAt = expiresAt,
|
||||
budgets = budgets
|
||||
)
|
||||
showAddSheet = false
|
||||
},
|
||||
@@ -196,7 +234,12 @@ fun NwcScreen(
|
||||
@Composable
|
||||
private fun NwcAddConnectionSheet(
|
||||
isCreating : Boolean,
|
||||
onCreate : (description: String, permissions: List<String>, expiresAt: Long) -> Unit,
|
||||
onCreate : (
|
||||
description : String,
|
||||
permissions : List<String>,
|
||||
expiresAt : Long,
|
||||
budgets : List<NwcNewBudget> // ← NEW
|
||||
) -> Unit,
|
||||
onDismiss : () -> Unit
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
@@ -207,7 +250,6 @@ private fun NwcAddConnectionSheet(
|
||||
}
|
||||
var neverExpires by remember { mutableStateOf(false) }
|
||||
|
||||
// Default expiry = now + 30 days, truncated to the minute
|
||||
val defaultExpiry = remember {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DAY_OF_YEAR, 30)
|
||||
@@ -216,11 +258,14 @@ private fun NwcAddConnectionSheet(
|
||||
}
|
||||
}
|
||||
var expiryCalendar by remember { mutableStateOf(defaultExpiry) }
|
||||
|
||||
var showDatePicker by remember { mutableStateOf(false) }
|
||||
var showTimePicker by remember { mutableStateOf(false) }
|
||||
|
||||
// ── Date picker dialog ────────────────────────────────────────────────────
|
||||
// ── Budget list state ──────────────────────────────────────────────────────
|
||||
var nextBudgetId by remember { mutableIntStateOf(0) }
|
||||
var budgets by remember { mutableStateOf(emptyList<BudgetDraft>()) }
|
||||
|
||||
// ── Date picker dialog ─────────────────────────────────────────────────────
|
||||
if (showDatePicker) {
|
||||
val datePickerState = rememberDatePickerState(
|
||||
initialSelectedDateMillis = expiryCalendar.timeInMillis
|
||||
@@ -243,12 +288,10 @@ private fun NwcAddConnectionSheet(
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDatePicker = false }) { Text("Cancel") }
|
||||
}
|
||||
) {
|
||||
DatePicker(state = datePickerState)
|
||||
}
|
||||
) { DatePicker(state = datePickerState) }
|
||||
}
|
||||
|
||||
// ── Time picker dialog ────────────────────────────────────────────────────
|
||||
// ── Time picker dialog ─────────────────────────────────────────────────────
|
||||
if (showTimePicker) {
|
||||
val timePickerState = rememberTimePickerState(
|
||||
initialHour = expiryCalendar.get(Calendar.HOUR_OF_DAY),
|
||||
@@ -292,7 +335,7 @@ private fun NwcAddConnectionSheet(
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
// ── Description ───────────────────────────────────────────────
|
||||
// ── Description ───────────────────────────────────────────────────
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
@@ -301,20 +344,14 @@ private fun NwcAddConnectionSheet(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
// ── Expiry ────────────────────────────────────────────────────
|
||||
// ── Expiry ────────────────────────────────────────────────────────
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
// horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Expires",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Text(text = "Expires", style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
// Date chip — hidden when "Never" is checked
|
||||
if (!neverExpires) {
|
||||
val dateLabel = remember(expiryCalendar) {
|
||||
SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
|
||||
@@ -331,10 +368,7 @@ private fun NwcAddConnectionSheet(
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
// Time chip
|
||||
val timeLabel = remember(expiryCalendar) {
|
||||
String.format(
|
||||
"%02d:%02d",
|
||||
@@ -356,18 +390,53 @@ private fun NwcAddConnectionSheet(
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
Checkbox(
|
||||
checked = neverExpires,
|
||||
onCheckedChange = { neverExpires = it }
|
||||
)
|
||||
Text(text = "Never", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
|
||||
// ── Budgets ───────────────────────────────────────────────────────
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Never",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
text = "Budgets",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
TextButton(
|
||||
onClick = {
|
||||
budgets = budgets + BudgetDraft(id = nextBudgetId)
|
||||
nextBudgetId++
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = "Add budget",
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Add budget")
|
||||
}
|
||||
}
|
||||
|
||||
budgets.forEach { draft ->
|
||||
BudgetRow(
|
||||
draft = draft,
|
||||
onChange = { updated ->
|
||||
budgets = budgets.map { if (it.id == draft.id) updated else it }
|
||||
},
|
||||
onRemove = {
|
||||
budgets = budgets.filter { it.id != draft.id }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ── Permissions ───────────────────────────────────────────────
|
||||
// ── Permissions ───────────────────────────────────────────────────
|
||||
Text(
|
||||
text = "Permissions",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
@@ -389,22 +458,21 @@ private fun NwcAddConnectionSheet(
|
||||
}
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = perm.displayLabel,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(text = perm.displayLabel, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Create button ─────────────────────────────────────────────
|
||||
// ── Create button ─────────────────────────────────────────────────
|
||||
Button(
|
||||
onClick = {
|
||||
val expiresAtSecs = if (neverExpires) 0L
|
||||
else expiryCalendar.timeInMillis / 1000L
|
||||
val validBudgets = budgets.mapNotNull { it.toNwcNewBudget() }
|
||||
onCreate(
|
||||
description.trim().ifBlank { "Unnamed" },
|
||||
selectedPermissions.toList(),
|
||||
expiresAtSecs
|
||||
expiresAtSecs,
|
||||
validBudgets
|
||||
)
|
||||
},
|
||||
enabled = selectedPermissions.isNotEmpty() && !isCreating,
|
||||
@@ -424,6 +492,96 @@ private fun NwcAddConnectionSheet(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun BudgetRow(
|
||||
draft : BudgetDraft,
|
||||
onChange : (BudgetDraft) -> Unit,
|
||||
onRemove : () -> Unit
|
||||
) {
|
||||
var dropdownExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
// Shared height for both fields so they are always flush
|
||||
val fieldHeight = 64.dp
|
||||
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
tonalElevation = 2.dp,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// ── Amount field ──────────────────────────────────────────────────
|
||||
OutlinedTextField(
|
||||
value = draft.amountSats,
|
||||
onValueChange = { onChange(draft.copy(amountSats = it.filter(Char::isDigit))) },
|
||||
label = { Text("Sats") },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(fieldHeight)
|
||||
)
|
||||
|
||||
// ── Refresh-window dropdown ───────────────────────────────────────
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = dropdownExpanded,
|
||||
onExpandedChange = { dropdownExpanded = it },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(fieldHeight)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = draft.refreshWindow.label,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Resets") },
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = dropdownExpanded)
|
||||
},
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth()
|
||||
.height(fieldHeight)
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = dropdownExpanded,
|
||||
onDismissRequest = { dropdownExpanded = false }
|
||||
) {
|
||||
BudgetRefreshWindow.entries.forEach { window ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(window.label) },
|
||||
onClick = {
|
||||
onChange(draft.copy(refreshWindow = window))
|
||||
dropdownExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Remove button ─────────────────────────────────────────────────
|
||||
IconButton(
|
||||
onClick = onRemove,
|
||||
modifier = Modifier.size(fieldHeight) // matches field height
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Remove budget",
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── Pairing URL dialog ────────────────────────────────────────────────────────
|
||||
|
||||
@Composable
|
||||
@@ -431,64 +589,77 @@ private fun NwcPairingUrlDialog(
|
||||
url : String,
|
||||
onDismiss : () -> Unit
|
||||
) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
var copied by remember { mutableStateOf(false) }
|
||||
// Brightness state lives here so the dialog owns the screen-brightness side-effect
|
||||
var brightnessOn by remember { mutableStateOf(false) }
|
||||
val window = (LocalContext.current as? Activity)?.window
|
||||
val context = LocalContext.current
|
||||
|
||||
// Boost / restore screen brightness when the toggle changes
|
||||
LaunchedEffect(brightnessOn) {
|
||||
window?.attributes = window.attributes?.apply {
|
||||
screenBrightness = if (brightnessOn) 1f
|
||||
else WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||
}
|
||||
}
|
||||
|
||||
// Restore brightness unconditionally when the dialog leaves composition
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
window?.attributes = window.attributes?.apply {
|
||||
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Connection created") },
|
||||
text = {
|
||||
title = { Text("Connection created") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
text = "Copy this connection string into your app. It will not be shown again.",
|
||||
text = "Scan or copy this connection string into your app. It will not be shown again.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Text(
|
||||
text = url,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
fontFamily = FontFamily.Monospace
|
||||
),
|
||||
modifier = Modifier.padding(12.dp),
|
||||
maxLines = 6,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = copied) {
|
||||
Text(
|
||||
text = "✓ Copied to clipboard",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
clipboard.setText(AnnotatedString(url))
|
||||
copied = true
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ContentCopy,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
QrDisplayCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
content = url,
|
||||
contentDescription = "NWC pairing QR code",
|
||||
clipLabel = "NWC connection string",
|
||||
shareTitle = "Share NWC connection string",
|
||||
textToCopy = url,
|
||||
brightnessOn = brightnessOn,
|
||||
onToggleBrightness = { brightnessOn = !brightnessOn },
|
||||
// No NFC for a one-shot pairing string
|
||||
isNfcEmulating = null,
|
||||
qrModifier = Modifier.fillMaxWidth(),
|
||||
onOpenUri = {
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
try {
|
||||
context.startActivity(
|
||||
Intent.createChooser(intent, "Open in app")
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
// No app installed that handles nostr+walletconnect://
|
||||
// Show a snackbar / toast as appropriate for your app
|
||||
Toast.makeText(
|
||||
context,
|
||||
"No compatible app found", // "No compatible app found"
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text("Copy")
|
||||
}
|
||||
},
|
||||
confirmButton = {}, // actions live inside QrDisplayCard
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text("Done") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NwcConnectionRow(
|
||||
|
||||
@@ -101,8 +101,8 @@ class NwcViewModel(
|
||||
budgets : List<NwcNewBudget> = emptyList()
|
||||
) {
|
||||
Timber.d("[$TAG] createConnection description='$description' permissions=$permissions")
|
||||
_isCreating.value = true
|
||||
viewModelScope.launch {
|
||||
_isCreating.value = true
|
||||
runCatching {
|
||||
val keypair = NwcKeyGenerator.generate()
|
||||
Timber.d("[$TAG] generated keypair pubkey=${keypair.pubKeyHex.take(16)}…")
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Nfc
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
@@ -55,6 +56,22 @@ import kotlinx.coroutines.launch
|
||||
* @param onToggleNfc Called when the NFC button is tapped.
|
||||
* @param nfcEnabled Whether the NFC button should be interactive (e.g. false when invoice expired).
|
||||
*/
|
||||
/**
|
||||
* QR image followed by Share / Copy / Brightness / NFC / Open-in-app icon-button row.
|
||||
*
|
||||
* @param content What to encode into the QR code.
|
||||
* @param contentDescription Accessibility label for the QR image.
|
||||
* @param clipLabel Label used when writing to the clipboard.
|
||||
* @param shareTitle Chooser title for the share intent.
|
||||
* @param textToCopy The raw string placed on the clipboard and shared.
|
||||
* @param brightnessOn Current brightness-boost state.
|
||||
* @param onToggleBrightness Called when the brightness button is tapped.
|
||||
* @param isNfcEmulating Current NFC emulation state (null = hide NFC button).
|
||||
* @param onToggleNfc Called when the NFC button is tapped.
|
||||
* @param nfcEnabled Whether the NFC button should be interactive.
|
||||
* @param onOpenUri When non-null, shows an "Open in app" icon button that
|
||||
* fires this callback. Pass null (default) to hide it.
|
||||
*/
|
||||
@Composable
|
||||
internal fun QrDisplayCard(
|
||||
modifier : Modifier = Modifier,
|
||||
@@ -65,9 +82,10 @@ internal fun QrDisplayCard(
|
||||
textToCopy : String,
|
||||
brightnessOn : Boolean,
|
||||
onToggleBrightness : () -> Unit,
|
||||
isNfcEmulating : Boolean? = null, // null → no NFC button shown
|
||||
isNfcEmulating : Boolean? = null,
|
||||
onToggleNfc : (() -> Unit)? = null,
|
||||
nfcEnabled : Boolean = true,
|
||||
onOpenUri : (() -> Unit)? = null, // ← NEW
|
||||
qrModifier : Modifier = Modifier.size(260.dp)
|
||||
) {
|
||||
val strings = LocalAppStrings.current
|
||||
@@ -91,7 +109,7 @@ internal fun QrDisplayCard(
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
)
|
||||
|
||||
// ── Action row: Share | Copy | Brightness | NFC ───────────────────────
|
||||
// ── Action row: Share | Copy | Brightness | NFC | Open ────────────────
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
|
||||
@@ -103,7 +121,7 @@ internal fun QrDisplayCard(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Share,
|
||||
contentDescription = strings.qrShare, // ← "Share"
|
||||
contentDescription = strings.qrShare,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
@@ -120,7 +138,7 @@ internal fun QrDisplayCard(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ContentCopy,
|
||||
contentDescription = strings.qrCopy, // ← "Copy"
|
||||
contentDescription = strings.qrCopy,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
@@ -129,8 +147,8 @@ internal fun QrDisplayCard(
|
||||
FilledTonalIconButton(onClick = onToggleBrightness) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.WbSunny,
|
||||
contentDescription = if (brightnessOn) strings.brightnessReduce // ← "Reduce brightness"
|
||||
else strings.brightnessBoost, // ← "Boost brightness"
|
||||
contentDescription = if (brightnessOn) strings.brightnessReduce
|
||||
else strings.brightnessBoost,
|
||||
tint = if (brightnessOn) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -145,17 +163,29 @@ internal fun QrDisplayCard(
|
||||
Icon(
|
||||
imageVector = if (isNfcEmulating) Icons.Filled.Nfc
|
||||
else Icons.Outlined.Nfc,
|
||||
contentDescription = if (isNfcEmulating) strings.qrNfcStop // ← "Stop NFC sharing"
|
||||
else strings.qrNfcStart, // ← "Share via NFC"
|
||||
contentDescription = if (isNfcEmulating) strings.qrNfcStop
|
||||
else strings.qrNfcStart,
|
||||
tint = if (isNfcEmulating) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Open in app — only shown when caller passes onOpenUri
|
||||
if (onOpenUri != null) {
|
||||
IconButton(onClick = onOpenUri) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
|
||||
contentDescription = strings.openInApp,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── QR generation ─────────────────────────────────────────────────────────────
|
||||
|
||||
private const val QR_BITMAP_SIZE = 512
|
||||
|
||||
Reference in New Issue
Block a user