localisation: nwc

This commit is contained in:
2026-06-12 20:50:36 +02:00
parent 201242c797
commit 262d5f068d
13 changed files with 387 additions and 95 deletions
@@ -379,6 +379,9 @@ val EnHomeStrings = AppStrings(
connectionRowExpires = "Expires", connectionRowExpires = "Expires",
connectionRowPubkey = "Public key", connectionRowPubkey = "Public key",
connectionNeverUsed = "Never", connectionNeverUsed = "Never",
lastUsedToday = "Last used today",
lastUsedYesterday = "Last used yesterday",
lastUsedDate = "Last used %s",
connectionNoExpiry = "Never", connectionNoExpiry = "Never",
connectionExpired = { date -> "Expired · $date" }, connectionExpired = { date -> "Expired · $date" },
connectionSectionPermissions = "Permissions", connectionSectionPermissions = "Permissions",
@@ -400,6 +403,10 @@ val EnHomeStrings = AppStrings(
permSignMessage = "Sign messages", permSignMessage = "Sign messages",
permPay = "Send payments", permPay = "Send payments",
permInvoice = "Create invoices", permInvoice = "Create invoices",
permLookup = "Lookup invoice status",
permHistory = "Read transaction history",
permBalance = "Read wallet balance",
permInfo = "Read account info",
permLookupHistory = "Lookup status & transaction history", permLookupHistory = "Lookup status & transaction history",
permBalanceInfo = "Read balance & account info", permBalanceInfo = "Read balance & account info",
permSendPayments = "Send payments", permSendPayments = "Send payments",
@@ -412,6 +419,57 @@ val EnHomeStrings = AppStrings(
revokeDialogBody = "This will permanently revoke this Nostr Wallet Connect key. Any app using it will lose access.", revokeDialogBody = "This will permanently revoke this Nostr Wallet Connect key. Any app using it will lose access.",
revokeDialogConfirm = "Revoke", revokeDialogConfirm = "Revoke",
revokeDialogDismiss = "Cancel", revokeDialogDismiss = "Cancel",
refreshDaily = "Daily",
refreshWeekly = "Weekly",
refreshMonthly = "Monthly",
refreshYearly = "Yearly",
refreshNever = "Never",
newConnection = "New connection",
labelHint = "Label (e.g. Amethyst, Bitrefill)",
expires = "Expires",
pickDate = "Pick date",
pickTime = "Pick time",
never = "Never",
budgets = "Budgets",
addBudget = "Add budget",
removeBudget = "Remove budget",
permissions = "Permissions",
unnamed = "Unnamed",
creating = "Creating…",
createConnection = "Create connection",
ok = "OK",
sats = "Sats",
resets = "Resets",
deleteConnection = "Delete connection",
unnamedConnection = "Unnamed connection",
neverUsed = "Never used",
expired = "Expired",
connectionCreated = "Connection created",
scanOrCopyHint = "Scan or copy this connection string into your app. It will not be shown again.",
nwcPairingQrCode = "NWC pairing QR code",
nwcConnectionString = "NWC connection string",
shareNwcConnectionString = "Share NWC connection string",
openInApp = "Open in app",
walletConnect = "Wallet Connect",
addConnection = "Add connection",
noCompatibleAppFound = "No compatible app found",
removeConnection = "Remove connection?",
removeConnectionDetail = "This will permanently revoke this Nostr Wallet Connect key. Any app using it will lose access.",
remove = "Remove",
noConnectionsYet = "No connections yet",
tapToAddHint = "Tap + to connect an app via Nostr Wallet Connect.",
somethingWentWrong = "Something went wrong",
retry = "Retry",
failedToLoadConnections = "Failed to load connections",
failedToCreateConnection = "Failed to create connection",
failedToDeleteConnection = "Failed to delete connection",
failedToLoadConnection = "Failed to load connection",
) )
) )
@@ -380,6 +380,9 @@ val EsHomeStrings = AppStrings(
connectionRowExpires = "Vence", connectionRowExpires = "Vence",
connectionRowPubkey = "Clave pública", connectionRowPubkey = "Clave pública",
connectionNeverUsed = "Nunca", connectionNeverUsed = "Nunca",
lastUsedToday = "Usada hoy",
lastUsedYesterday = "Usada ayer",
lastUsedDate = "Usada %s",
connectionNoExpiry = "Sin vencimiento", connectionNoExpiry = "Sin vencimiento",
connectionExpired = { date -> "Vencido · $date" }, connectionExpired = { date -> "Vencido · $date" },
connectionSectionPermissions = "Permisos", connectionSectionPermissions = "Permisos",
@@ -401,6 +404,10 @@ val EsHomeStrings = AppStrings(
permSignMessage = "Firmar mensajes", permSignMessage = "Firmar mensajes",
permPay = "Enviar pagos", permPay = "Enviar pagos",
permInvoice = "Crear facturas", permInvoice = "Crear facturas",
permLookup = "Consultar estado de facturas",
permHistory = "Leer historial de transacciones",
permBalance = "Leer saldo del monedero",
permInfo = "Leer información de cuenta",
permLookupHistory = "Consultar estado e historial de transacciones", permLookupHistory = "Consultar estado e historial de transacciones",
permBalanceInfo = "Ver saldo e información de cuenta", permBalanceInfo = "Ver saldo e información de cuenta",
permSendPayments = "Enviar pagos", permSendPayments = "Enviar pagos",
@@ -413,5 +420,56 @@ val EsHomeStrings = AppStrings(
revokeDialogBody = "Esto revocará permanentemente esta clave Nostr Wallet Connect. Cualquier app que la use perderá el acceso.", revokeDialogBody = "Esto revocará permanentemente esta clave Nostr Wallet Connect. Cualquier app que la use perderá el acceso.",
revokeDialogConfirm = "Revocar", revokeDialogConfirm = "Revocar",
revokeDialogDismiss = "Cancelar", revokeDialogDismiss = "Cancelar",
refreshDaily = "Diario",
refreshWeekly = "Semanal",
refreshMonthly = "Mensual",
refreshYearly = "Anual",
refreshNever = "Nunca",
newConnection = "Nueva conexión",
labelHint = "Etiqueta (p.ej. Amethyst, Bitrefill)",
expires = "Caduca",
pickDate = "Elegir fecha",
pickTime = "Elegir hora",
never = "Nunca",
budgets = "Presupuestos",
addBudget = "Añadir presupuesto",
removeBudget = "Eliminar presupuesto",
permissions = "Permisos",
unnamed = "Sin nombre",
creating = "Creando…",
createConnection = "Crear conexión",
ok = "OK",
sats = "Sats",
resets = "Se reinicia",
deleteConnection = "Eliminar conexión",
unnamedConnection = "Conexión sin nombre",
neverUsed = "Nunca usada",
expired = "Caducada",
connectionCreated = "Conexión creada",
scanOrCopyHint = "Escanea o copia esta cadena de conexión en tu app. No se volverá a mostrar.",
nwcPairingQrCode = "Código QR de emparejamiento NWC",
nwcConnectionString = "Cadena de conexión NWC",
shareNwcConnectionString = "Compartir cadena de conexión NWC",
openInApp = "Abrir en app",
walletConnect = "Wallet Connect",
addConnection = "Añadir conexión",
noCompatibleAppFound = "No se encontró una app compatible",
removeConnection = "¿Eliminar conexión?",
removeConnectionDetail = "Esto revocará permanentemente esta clave de Nostr Wallet Connect. Cualquier app que la use perderá el acceso.",
remove = "Eliminar",
noConnectionsYet = "Aún no hay conexiones",
tapToAddHint = "Toca + para conectar una app vía Nostr Wallet Connect.",
somethingWentWrong = "Algo salió mal",
retry = "Reintentar",
failedToLoadConnections = "Error al cargar las conexiones",
failedToCreateConnection = "Error al crear la conexión",
failedToDeleteConnection = "Error al eliminar la conexión",
failedToLoadConnection = "Error al cargar la conexión",
) )
) )
@@ -376,6 +376,9 @@ val EuStrings = AppStrings(
connectionSectionDetails = "Xehetasunak", connectionSectionDetails = "Xehetasunak",
connectionRowCreated = "Konektatuta", connectionRowCreated = "Konektatuta",
connectionRowLastUsed = "Azken erabilera", connectionRowLastUsed = "Azken erabilera",
lastUsedToday = "Duela gutxi erabilia", // "used recently" (idiomatic for today)
lastUsedYesterday = "Atzo erabilia", // "used yesterday"
lastUsedDate = "Duela %s erabilia", // "used [date] ago"
connectionRowExpires = "Iraungitze-data", connectionRowExpires = "Iraungitze-data",
connectionRowPubkey = "Gako publikoa", connectionRowPubkey = "Gako publikoa",
connectionNeverUsed = "Inoiz ez", connectionNeverUsed = "Inoiz ez",
@@ -400,6 +403,10 @@ val EuStrings = AppStrings(
permSignMessage = "Mezuak sinatu", permSignMessage = "Mezuak sinatu",
permPay = "Ordainketak bidali", permPay = "Ordainketak bidali",
permInvoice = "Fakturak sortu", permInvoice = "Fakturak sortu",
permLookup = "Kontsultatu faktura-egoera",
permHistory = "Irakurri transakzio-historiala",
permBalance = "Irakurri zorroaren saldoa",
permInfo = "Irakurri kontu-informazioa",
permLookupHistory = "Egoera eta transakzio-historia kontsultatu", permLookupHistory = "Egoera eta transakzio-historia kontsultatu",
permBalanceInfo = "Saldoa eta kontuaren informazioa ikusi", permBalanceInfo = "Saldoa eta kontuaren informazioa ikusi",
permSendPayments = "Ordainketak bidali", permSendPayments = "Ordainketak bidali",
@@ -412,5 +419,56 @@ val EuStrings = AppStrings(
revokeDialogBody = "Honek Nostr Wallet Connect gako hau behin betiko baliogabetuko du. Erabiltzen duen edozein aplikaziok sarbidea galduko du.", revokeDialogBody = "Honek Nostr Wallet Connect gako hau behin betiko baliogabetuko du. Erabiltzen duen edozein aplikaziok sarbidea galduko du.",
revokeDialogConfirm = "Baliogabetu", revokeDialogConfirm = "Baliogabetu",
revokeDialogDismiss = "Utzi", revokeDialogDismiss = "Utzi",
refreshDaily = "Egunero",
refreshWeekly = "Astero",
refreshMonthly = "Hilabetero",
refreshYearly = "Urtero",
refreshNever = "Inoiz ez",
newConnection = "Konexio berria",
labelHint = "Etiketa (adb. Amethyst, Bitrefill)",
expires = "Iraungitzen da",
pickDate = "Aukeratu data",
pickTime = "Aukeratu ordua",
never = "Inoiz ez",
budgets = "Aurrekontuak",
addBudget = "Gehitu aurrekontua",
removeBudget = "Kendu aurrekontua",
permissions = "Baimenak",
unnamed = "Izengabea",
creating = "Sortzen…",
createConnection = "Sortu konexioa",
ok = "Ados",
sats = "Sats",
resets = "Berrezarri",
deleteConnection = "Ezabatu konexioa",
unnamedConnection = "Izengabeko konexioa",
neverUsed = "Inoiz erabili gabea",
expired = "Iraungitua",
connectionCreated = "Konexioa sortuta",
scanOrCopyHint = "Eskaneatu edo kopiatu konexio-kate hau zure app-ean. Ez da berriro erakutsiko.",
nwcPairingQrCode = "NWC parekatzeko QR kodea",
nwcConnectionString = "NWC konexio-katea",
shareNwcConnectionString = "Partekatu NWC konexio-katea",
openInApp = "Ireki app-ean",
walletConnect = "Wallet Connect",
addConnection = "Gehitu konexioa",
noCompatibleAppFound = "Ez da app bateragarririk aurkitu",
removeConnection = "Kendu konexioa?",
removeConnectionDetail = "Honek Nostr Wallet Connect gako hau behin betiko indargabetuko du. Erabiltzen duen edozein app-ek sarbidea galduko du.",
remove = "Kendu",
noConnectionsYet = "Oraindik konexiorik ez",
tapToAddHint = "Sakatu + app bat Nostr Wallet Connect bidez konektatzeko.",
somethingWentWrong = "Zerbait gaizki atera da",
retry = "Saiatu berriro",
failedToLoadConnections = "Huts egin du konexioak kargatzean",
failedToCreateConnection = "Huts egin du konexioa sortzean",
failedToDeleteConnection = "Huts egin du konexioa ezabatzean",
failedToLoadConnection = "Huts egin du konexioa kargatzean",
) )
) )
@@ -383,6 +383,9 @@ data class NwcStrings(
val connectionRowExpires : String, val connectionRowExpires : String,
val connectionRowPubkey : String, val connectionRowPubkey : String,
val connectionNeverUsed : String, val connectionNeverUsed : String,
val lastUsedToday : String, // e.g. "Last used today"
val lastUsedYesterday : String, // e.g. "Last used yesterday"
val lastUsedDate : String, // e.g. "Last used %s" — %s is a formatted date
val connectionNoExpiry : String, val connectionNoExpiry : String,
/** "Expired · {date}" */ /** "Expired · {date}" */
val connectionExpired : (date: String) -> String, val connectionExpired : (date: String) -> String,
@@ -408,10 +411,14 @@ data class NwcStrings(
val permSignMessage : String, val permSignMessage : String,
val permPay : String, // "Send payments" val permPay : String, // "Send payments"
val permInvoice : String, // "Create invoices" val permInvoice : String, // "Create invoices"
val permLookupHistory: String, // "Lookup status & transaction history" val permLookup : String,
val permHistory : String,
val permBalance : String,
val permInfo : String,
val permLookupHistory : String, // "Lookup status & transaction history"
val permBalanceInfo : String, // "Read balance & account info" val permBalanceInfo : String, // "Read balance & account info"
val permSendPayments: String, val permSendPayments : String,
val permCreateInvoices: String, val permCreateInvoices : String,
val permLookupInvoiceStatus: String, val permLookupInvoiceStatus: String,
val permReadTransactionHistory: String, val permReadTransactionHistory: String,
val permReadWalletBalance: String, val permReadWalletBalance: String,
@@ -420,4 +427,56 @@ data class NwcStrings(
val revokeDialogBody : String, val revokeDialogBody : String,
val revokeDialogConfirm : String, val revokeDialogConfirm : String,
val revokeDialogDismiss : String, val revokeDialogDismiss : String,
) val refreshDaily : String,
val refreshWeekly : String,
val refreshMonthly : String,
val refreshYearly : String,
val refreshNever : String,
// New Connection
val newConnection : String,
val labelHint : String,
val expires : String,
val pickDate : String,
val pickTime : String,
val never : String,
val budgets : String,
val addBudget : String,
val removeBudget : String,
val permissions : String,
val unnamed : String,
val creating : String,
val createConnection : String,
val ok : String,
val sats : String,
val resets : String,
val deleteConnection : String,
val unnamedConnection : String,
val neverUsed : String,
val expired : String,
val connectionCreated : String,
val scanOrCopyHint : String,
val nwcPairingQrCode : String,
val nwcConnectionString : String,
val shareNwcConnectionString : String,
val openInApp : String,
val walletConnect : String,
val addConnection : String,
val noCompatibleAppFound : String,
val removeConnection : String,
val removeConnectionDetail: String,
val remove : String,
val noConnectionsYet : String,
val tapToAddHint : String,
val somethingWentWrong : String,
val retry : String,
val failedToLoadConnections : String,
val failedToCreateConnection : String,
val failedToDeleteConnection : String,
val failedToLoadConnection : String,
)
@@ -461,7 +461,7 @@ fun WalletScreen(
val connectionState by nwcVm.connectionDetailState.collectAsStateWithLifecycle() val connectionState by nwcVm.connectionDetailState.collectAsStateWithLifecycle()
LaunchedEffect(pubkey) { nwcVm.loadConnectionDetail(pubkey) } LaunchedEffect(pubkey) { nwcVm.loadConnectionDetail(pubkey, strings) }
DisposableEffect(Unit) { onDispose { nwcVm.clearConnectionDetail() } } DisposableEffect(Unit) { onDispose { nwcVm.clearConnectionDetail() } }
when (val state = connectionState) { when (val state = connectionState) {
@@ -491,7 +491,7 @@ fun WalletScreen(
budgets = state.budgets, budgets = state.budgets,
onBack = { navController.popBackStack() }, onBack = { navController.popBackStack() },
onRevoke = { onRevoke = {
nwcVm.deleteConnection(pubkey) nwcVm.deleteConnection(pubkey, strings)
navController.popBackStack() navController.popBackStack()
} }
) )
@@ -16,6 +16,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.bitcointxoko.gudariwallet.api.NwcNewBudget import com.bitcointxoko.gudariwallet.api.NwcNewBudget
import com.bitcointxoko.gudariwallet.i18n.AppStrings
import com.bitcointxoko.gudariwallet.strings
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -41,6 +43,16 @@ private class AddConnectionState {
expiry.atZone(ZoneId.systemDefault()).toEpochSecond() expiry.atZone(ZoneId.systemDefault()).toEpochSecond()
} }
private fun NwcPermission.displayLabel(strings: AppStrings): String = when (serverKey) {
"pay" -> strings.nwc.permPay
"invoice" -> strings.nwc.permInvoice
"lookup" -> strings.nwc.permLookup
"history" -> strings.nwc.permHistory
"balance" -> strings.nwc.permBalance
"info" -> strings.nwc.permInfo
else -> serverKey
}
// ── Sheet ───────────────────────────────────────────────────────────────────── // ── Sheet ─────────────────────────────────────────────────────────────────────
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -58,6 +70,7 @@ internal fun NwcAddConnectionSheet(
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val state = remember { AddConnectionState() } val state = remember { AddConnectionState() }
if (state.showDatePicker) { if (state.showDatePicker) {
val initialMillis = state.expiry val initialMillis = state.expiry
.atZone(ZoneId.systemDefault()) .atZone(ZoneId.systemDefault())
@@ -81,10 +94,10 @@ internal fun NwcAddConnectionSheet(
.withDayOfMonth(picked.dayOfMonth) .withDayOfMonth(picked.dayOfMonth)
} }
state.showDatePicker = false state.showDatePicker = false
}) { Text("OK") } }) { Text(strings.nwc.ok) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { state.showDatePicker = false }) { Text("Cancel") } TextButton(onClick = { state.showDatePicker = false }) { Text(strings.nwc.cancel) }
} }
) { DatePicker(state = datePickerState) } ) { DatePicker(state = datePickerState) }
} }
@@ -105,10 +118,10 @@ internal fun NwcAddConnectionSheet(
.withSecond(0) .withSecond(0)
.withNano(0) .withNano(0)
state.showTimePicker = false state.showTimePicker = false
}) { Text("OK") } }) { Text(strings.nwc.ok) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { state.showTimePicker = false }) { Text("Cancel") } TextButton(onClick = { state.showTimePicker = false }) { Text(strings.nwc.cancel) }
}, },
text = { TimePicker(state = timePickerState) } text = { TimePicker(state = timePickerState) }
) )
@@ -127,7 +140,7 @@ internal fun NwcAddConnectionSheet(
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Text( Text(
text = "New connection", text = strings.nwc.newConnection,
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
@@ -135,7 +148,7 @@ internal fun NwcAddConnectionSheet(
OutlinedTextField( OutlinedTextField(
value = state.description, value = state.description,
onValueChange = { state.description = it }, onValueChange = { state.description = it },
label = { Text("Label (e.g. Amethyst, Bitrefill)") }, label = { Text(strings.nwc.labelHint) },
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
@@ -145,7 +158,7 @@ internal fun NwcAddConnectionSheet(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text(text = "Expires", style = MaterialTheme.typography.bodyMedium) Text(text = strings.nwc.expires, style = MaterialTheme.typography.bodyMedium)
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
if (!state.neverExpires) { if (!state.neverExpires) {
@@ -158,7 +171,7 @@ internal fun NwcAddConnectionSheet(
icon = { icon = {
Icon( Icon(
imageVector = Icons.Default.CalendarToday, imageVector = Icons.Default.CalendarToday,
contentDescription = "Pick date", contentDescription = strings.nwc.pickDate,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
} }
@@ -173,7 +186,7 @@ internal fun NwcAddConnectionSheet(
icon = { icon = {
Icon( Icon(
imageVector = Icons.Default.Schedule, imageVector = Icons.Default.Schedule,
contentDescription = "Pick time", contentDescription = strings.nwc.pickTime,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
} }
@@ -185,7 +198,7 @@ internal fun NwcAddConnectionSheet(
checked = state.neverExpires, checked = state.neverExpires,
onCheckedChange = { state.neverExpires = it } onCheckedChange = { state.neverExpires = it }
) )
Text(text = "Never", style = MaterialTheme.typography.bodyMedium) Text(text = strings.nwc.never, style = MaterialTheme.typography.bodyMedium)
} }
Row( Row(
@@ -193,7 +206,7 @@ internal fun NwcAddConnectionSheet(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = "Budgets", text = strings.nwc.budgets,
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@@ -206,11 +219,11 @@ internal fun NwcAddConnectionSheet(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Add, imageVector = Icons.Default.Add,
contentDescription = "Add budget", contentDescription = strings.nwc.addBudget,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
Text("Add budget") Text(strings.nwc.addBudget)
} }
} }
@@ -230,7 +243,7 @@ internal fun NwcAddConnectionSheet(
// ── Permissions ────────────────────────────────────────────────── // ── Permissions ──────────────────────────────────────────────────
Text( Text(
text = "Permissions", text = strings.nwc.permissions,
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@@ -251,11 +264,11 @@ internal fun NwcAddConnectionSheet(
} }
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text(text = perm.displayLabel, style = MaterialTheme.typography.bodyMedium) Text(text = perm.displayLabel(strings), style = MaterialTheme.typography.bodyMedium)
} }
} }
} }
val unnamed = strings.nwc.unnamed
// ── Create button ───────────────────────────────────────────────── // ── Create button ─────────────────────────────────────────────────
Button( Button(
onClick = { onClick = {
@@ -263,7 +276,7 @@ internal fun NwcAddConnectionSheet(
else state.expiryEpochSeconds() // #10 else state.expiryEpochSeconds() // #10
val validBudgets = state.budgets.mapNotNull { it.toNwcNewBudget() } val validBudgets = state.budgets.mapNotNull { it.toNwcNewBudget() }
onCreate( onCreate(
state.description.trim().ifBlank { "Unnamed" }, state.description.trim().ifBlank { unnamed },
state.selectedPermissions.toList(), state.selectedPermissions.toList(),
expiresAtSecs, expiresAtSecs,
validBudgets validBudgets
@@ -280,7 +293,7 @@ internal fun NwcAddConnectionSheet(
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
} }
Text(if (isCreating) "Creating…" else "Create connection") Text(if (isCreating) strings.nwc.creating else strings.nwc.createConnection)
} }
} }
} }
@@ -288,6 +301,15 @@ internal fun NwcAddConnectionSheet(
// ── Budget row ──────────────────────────────────────────────────────────────── // ── Budget row ────────────────────────────────────────────────────────────────
@Composable
fun BudgetRefreshWindow.displayLabel(strings: AppStrings): String = when (this) {
BudgetRefreshWindow.DAILY -> strings.nwc.refreshDaily
BudgetRefreshWindow.WEEKLY -> strings.nwc.refreshWeekly
BudgetRefreshWindow.MONTHLY -> strings.nwc.refreshMonthly
BudgetRefreshWindow.YEARLY -> strings.nwc.refreshYearly
BudgetRefreshWindow.NEVER -> strings.nwc.refreshNever
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun BudgetRow( private fun BudgetRow(
@@ -313,7 +335,7 @@ private fun BudgetRow(
OutlinedTextField( OutlinedTextField(
value = draft.amountSats, value = draft.amountSats,
onValueChange = { onChange(draft.copy(amountSats = it.filter(Char::isDigit))) }, onValueChange = { onChange(draft.copy(amountSats = it.filter(Char::isDigit))) },
label = { Text("Sats") }, label = { Text(strings.nwc.sats) },
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f).height(fieldHeight) modifier = Modifier.weight(1f).height(fieldHeight)
@@ -325,10 +347,10 @@ private fun BudgetRow(
modifier = Modifier.weight(1f).height(fieldHeight) modifier = Modifier.weight(1f).height(fieldHeight)
) { ) {
OutlinedTextField( OutlinedTextField(
value = draft.refreshWindow.label, value = draft.refreshWindow.displayLabel(strings),
onValueChange = {}, onValueChange = {},
readOnly = true, readOnly = true,
label = { Text("Resets") }, label = { Text(strings.nwc.resets) },
trailingIcon = { trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = dropdownExpanded) ExposedDropdownMenuDefaults.TrailingIcon(expanded = dropdownExpanded)
}, },
@@ -343,7 +365,7 @@ private fun BudgetRow(
) { ) {
BudgetRefreshWindow.entries.forEach { window -> BudgetRefreshWindow.entries.forEach { window ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(window.label) }, text = { Text(window.displayLabel(strings)) },
onClick = { onClick = {
onChange(draft.copy(refreshWindow = window)) onChange(draft.copy(refreshWindow = window))
dropdownExpanded = false dropdownExpanded = false
@@ -359,7 +381,7 @@ private fun BudgetRow(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = "Remove budget", contentDescription = strings.nwc.removeBudget,
tint = MaterialTheme.colorScheme.error tint = MaterialTheme.colorScheme.error
) )
} }
@@ -1,5 +1,7 @@
package com.bitcointxoko.gudariwallet.ui.nwc package com.bitcointxoko.gudariwallet.ui.nwc
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -17,16 +19,19 @@ import com.bitcointxoko.gudariwallet.api.NwcGetResponse
import com.bitcointxoko.gudariwallet.strings import com.bitcointxoko.gudariwallet.strings
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
import com.bitcointxoko.gudariwallet.util.formatEpochShort import com.bitcointxoko.gudariwallet.util.formatEpochShort
import com.bitcointxoko.gudariwallet.util.isToday
import com.bitcointxoko.gudariwallet.util.isYesterday
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Instant import java.time.Instant
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable @Composable
internal fun NwcConnectionRow( internal fun NwcConnectionRow(
connection : NwcGetResponse, connection: NwcGetResponse,
isDeleting : Boolean, isDeleting: Boolean,
onDeleteConfirm : () -> Unit, onDeleteConfirm: () -> Unit,
onConnectionClick: () -> Unit, onConnectionClick: () -> Unit,
modifier : Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val key = connection.data val key = connection.data
val expired = key.expires_at > 0 && key.expires_at < Instant.now().epochSecond val expired = key.expires_at > 0 && key.expires_at < Instant.now().epochSecond
@@ -85,7 +90,7 @@ internal fun NwcConnectionRow(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Delete, imageVector = Icons.Default.Delete,
contentDescription = "Delete connection", contentDescription = strings.nwc.deleteConnection,
tint = MaterialTheme.colorScheme.onError.copy(alpha = bgAlpha) tint = MaterialTheme.colorScheme.onError.copy(alpha = bgAlpha)
) )
} }
@@ -111,7 +116,7 @@ internal fun NwcConnectionRow(
Spacer(Modifier.width(12.dp)) Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = key.description.ifBlank { "Unnamed connection" }, text = key.description.ifBlank { strings.nwc.unnamedConnection },
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
@@ -121,9 +126,15 @@ internal fun NwcConnectionRow(
MaterialTheme.colorScheme.onSurface MaterialTheme.colorScheme.onSurface
) )
Spacer(Modifier.height(2.dp)) Spacer(Modifier.height(2.dp))
val lastUsedText = when {
key.last_used <= 0L -> strings.nwc.neverUsed
isToday(key.last_used) -> strings.nwc.lastUsedToday
isYesterday(key.last_used) -> strings.nwc.lastUsedYesterday
else -> strings.nwc.lastUsedDate.format(formatEpochShort(key.last_used, strings.today, strings.yesterday))
}
Text( Text(
text = if (key.last_used <= 0L) "Never used" text = if (key.last_used <= 0L) strings.nwc.neverUsed
else "Last used ${formatEpochShort(key.last_used, strings.today, strings.yesterday)}", else lastUsedText,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@@ -135,7 +146,7 @@ internal fun NwcConnectionRow(
contentColor = semantic.onErrorContainer contentColor = semantic.onErrorContainer
) { ) {
Text( Text(
text = "Expired", text = strings.nwc.expired,
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
) )
@@ -5,17 +5,16 @@ import com.bitcointxoko.gudariwallet.api.NwcNewBudget
// ── Available permissions ──────────────────────────────────────────── // ── Available permissions ────────────────────────────────────────────
internal data class NwcPermission( internal data class NwcPermission(
val serverKey : String, // what we send in the permissions array val serverKey : String, // what we send in the permissions array
val displayLabel : String, // shown in the UI
val default : Boolean // pre-checked by default val default : Boolean // pre-checked by default
) )
internal val ALL_PERMISSIONS = listOf( internal val ALL_PERMISSIONS = listOf(
NwcPermission("pay", "Send payments", default = true), NwcPermission("pay", default = true),
NwcPermission("invoice", "Create invoices", default = false), NwcPermission("invoice", default = false),
NwcPermission("lookup", "Lookup invoice status", default = false), NwcPermission("lookup", default = false),
NwcPermission("history", "Read transaction history", default = false), NwcPermission("history", default = false),
NwcPermission("balance", "Read wallet balance", default = false), NwcPermission("balance", default = false),
NwcPermission("info", "Read account info", default = false) NwcPermission("info", default = false)
) )
// ── Budget ──────────────────────────────────────────────────────────── // ── Budget ────────────────────────────────────────────────────────────
@@ -24,12 +23,12 @@ internal val ALL_PERMISSIONS = listOf(
* Transient UI state for a single budget row in the "Add connection" sheet. * Transient UI state for a single budget row in the "Add connection" sheet.
* Converted to [NwcNewBudget] on submit. * Converted to [NwcNewBudget] on submit.
*/ */
enum class BudgetRefreshWindow(val label: String, val seconds: Int) { enum class BudgetRefreshWindow(val seconds: Int) {
DAILY ("Daily", 86_400), DAILY (86_400),
WEEKLY ("Weekly", 604_800), WEEKLY (604_800),
MONTHLY ("Monthly", 2_592_000), MONTHLY (2_592_000),
YEARLY ("Yearly", 31_536_000), YEARLY (31_536_000),
NEVER ("Never", 0); NEVER (0);
} }
data class BudgetDraft( data class BudgetDraft(
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri import androidx.core.net.toUri
import com.bitcointxoko.gudariwallet.strings
import com.bitcointxoko.gudariwallet.ui.common.rememberBrightnessController import com.bitcointxoko.gudariwallet.ui.common.rememberBrightnessController
import com.bitcointxoko.gudariwallet.ui.common.QrDisplayCard import com.bitcointxoko.gudariwallet.ui.common.QrDisplayCard
@@ -29,20 +30,21 @@ internal fun NwcPairingUrlDialog(
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Connection created") }, title = { Text(strings.nwc.connectionCreated) },
text = { text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text( Text(
text = "Scan or copy this connection string into your app. It will not be shown again.", text = strings.nwc.scanOrCopyHint,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
val strings = strings
QrDisplayCard( QrDisplayCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
content = url, content = url,
contentDescription = "NWC pairing QR code", contentDescription = strings.nwc.nwcPairingQrCode,
clipLabel = "NWC connection string", clipLabel = strings.nwc.nwcConnectionString,
shareTitle = "Share NWC connection string", shareTitle = strings.nwc.shareNwcConnectionString,
textToCopy = url, textToCopy = url,
brightnessOn = brightness.isOn, brightnessOn = brightness.isOn,
onToggleBrightness = brightness.toggle, onToggleBrightness = brightness.toggle,
@@ -52,7 +54,7 @@ internal fun NwcPairingUrlDialog(
val intent = Intent(Intent.ACTION_VIEW, url.toUri()) val intent = Intent(Intent.ACTION_VIEW, url.toUri())
try { try {
context.startActivity( context.startActivity(
Intent.createChooser(intent, "Open in app") Intent.createChooser(intent, strings.nwc.openInApp)
) )
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
onNoAppFound() // #17: no Toast — caller handles it onNoAppFound() // #17: no Toast — caller handles it
@@ -63,7 +65,7 @@ internal fun NwcPairingUrlDialog(
}, },
confirmButton = {}, // actions live inside QrDisplayCard confirmButton = {}, // actions live inside QrDisplayCard
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text("Done") } TextButton(onClick = onDismiss) { Text(strings.nwc.done) }
} }
) )
} }
@@ -1,5 +1,7 @@
package com.bitcointxoko.gudariwallet.ui.nwc package com.bitcointxoko.gudariwallet.ui.nwc
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -13,8 +15,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitcointxoko.gudariwallet.LocalAppStrings
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun NwcScreen( fun NwcScreen(
@@ -30,8 +34,13 @@ fun NwcScreen(
val pendingDeletePubkey by viewModel.pendingDeletePubkey.collectAsStateWithLifecycle() val pendingDeletePubkey by viewModel.pendingDeletePubkey.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val strings = LocalAppStrings.current
val refresh = { viewModel.loadConnections(includeExpired = true) } val refresh = { viewModel.loadConnections(includeExpired = true, strings) }
LaunchedEffect(Unit) {
viewModel.loadConnections(includeExpired = true, strings)
}
LaunchedEffect(pairingUrl) { LaunchedEffect(pairingUrl) {
if (pairingUrl != null) { if (pairingUrl != null) {
@@ -42,17 +51,17 @@ fun NwcScreen(
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text("Wallet Connect") }, title = { Text(strings.nwc.walletConnect) },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = strings.nwc.back)
} }
} }
) )
}, },
floatingActionButton = { floatingActionButton = {
FloatingActionButton(onClick = { viewModel.openAddSheet() }) { FloatingActionButton(onClick = { viewModel.openAddSheet() }) {
Icon(Icons.Default.Add, contentDescription = "Add connection") Icon(Icons.Default.Add, contentDescription = strings.nwc.addConnection)
} }
}, },
snackbarHost = { SnackbarHost(snackbarHostState) } snackbarHost = { SnackbarHost(snackbarHostState) }
@@ -102,7 +111,7 @@ fun NwcScreen(
connection, connection,
isDeleting, isDeleting,
onDeleteConfirm = { viewModel.requestDelete(connection.data.pubkey) }, onDeleteConfirm = { viewModel.requestDelete(connection.data.pubkey) },
onConnectionClick = { onConnectionClick(connection.data.pubkey) } { onConnectionClick(connection.data.pubkey) }
) )
HorizontalDivider( HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
@@ -125,7 +134,8 @@ fun NwcScreen(
description = description, description = description,
permissions = permissions, permissions = permissions,
expiresAt = expiresAt, expiresAt = expiresAt,
budgets = budgets budgets = budgets,
strings = strings
) )
}, },
onDismiss = { viewModel.closeAddSheet() } onDismiss = { viewModel.closeAddSheet() }
@@ -134,12 +144,13 @@ fun NwcScreen(
// ── Pairing URL dialog ───────────────────────────────────────────────────── // ── Pairing URL dialog ─────────────────────────────────────────────────────
pairingUrl?.let { url -> pairingUrl?.let { url ->
val strings = strings
NwcPairingUrlDialog( NwcPairingUrlDialog(
url = url, url = url,
onDismiss = { viewModel.clearPairingUrl() }, onDismiss = { viewModel.clearPairingUrl() },
onNoAppFound = { onNoAppFound = {
scope.launch { scope.launch {
snackbarHostState.showSnackbar("No compatible app found") snackbarHostState.showSnackbar(strings.nwc.noCompatibleAppFound)
} }
} }
) )
@@ -149,15 +160,15 @@ fun NwcScreen(
pendingDeletePubkey?.let { pendingDeletePubkey?.let {
AlertDialog( AlertDialog(
onDismissRequest = { viewModel.cancelDelete() }, onDismissRequest = { viewModel.cancelDelete() },
title = { Text("Remove connection?") }, title = { Text(strings.nwc.removeConnection) },
text = { Text("This will permanently revoke this Nostr Wallet Connect key. Any app using it will lose access.") }, text = { Text(strings.nwc.removeConnectionDetail) },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { viewModel.confirmDelete() } onClick = { viewModel.confirmDelete(strings) }
) { Text("Remove", color = MaterialTheme.colorScheme.error) } ) { Text(strings.nwc.remove, color = MaterialTheme.colorScheme.error) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { viewModel.cancelDelete() }) { Text("Cancel") } TextButton(onClick = { viewModel.cancelDelete() }) { Text(strings.nwc.cancel) }
} }
) )
} }
@@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.bitcointxoko.gudariwallet.strings
// ── Empty state ────────────────────────────────────────────────────── // ── Empty state ──────────────────────────────────────────────────────
@@ -21,10 +22,10 @@ internal fun NwcEmptyContent(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Text("No connections yet", style = MaterialTheme.typography.titleMedium) Text(strings.nwc.noConnectionsYet, style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text( Text(
"Tap + to connect an app via Nostr Wallet Connect.", strings.nwc.tapToAddHint,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@@ -32,7 +33,7 @@ internal fun NwcEmptyContent(
Button(onClick = onAddNew) { Button(onClick = onAddNew) {
Icon(Icons.Default.Add, contentDescription = null) Icon(Icons.Default.Add, contentDescription = null)
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text("Add connection") Text(strings.nwc.addConnection)
} }
} }
} }
@@ -51,7 +52,7 @@ internal fun NwcErrorContent(
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Text( Text(
"Something went wrong", strings.nwc.somethingWentWrong,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.error color = MaterialTheme.colorScheme.error
) )
@@ -62,6 +63,6 @@ internal fun NwcErrorContent(
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
OutlinedButton(onClick = onRetry) { Text("Retry") } OutlinedButton(onClick = onRetry) { Text(strings.nwc.retry) }
} }
} }
@@ -3,14 +3,16 @@ package com.bitcointxoko.gudariwallet.ui.nwc
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.bitcointxoko.gudariwallet.LocalAppStrings
import com.bitcointxoko.gudariwallet.api.NwcGetResponse import com.bitcointxoko.gudariwallet.api.NwcGetResponse
import com.bitcointxoko.gudariwallet.api.NwcNewBudget import com.bitcointxoko.gudariwallet.api.NwcNewBudget
import com.bitcointxoko.gudariwallet.api.NwcRegistrationRequest import com.bitcointxoko.gudariwallet.api.NwcRegistrationRequest
import com.bitcointxoko.gudariwallet.data.NwcCacheRepository import com.bitcointxoko.gudariwallet.data.NwcCacheRepository
import com.bitcointxoko.gudariwallet.data.WalletRepository import com.bitcointxoko.gudariwallet.data.WalletRepository
import com.bitcointxoko.gudariwallet.data.db.toDomain
import com.bitcointxoko.gudariwallet.data.toDomain import com.bitcointxoko.gudariwallet.data.toDomain
import com.bitcointxoko.gudariwallet.data.toGetResponse import com.bitcointxoko.gudariwallet.data.toGetResponse
import com.bitcointxoko.gudariwallet.i18n.AppStrings
import com.bitcointxoko.gudariwallet.util.NwcKeyGenerator import com.bitcointxoko.gudariwallet.util.NwcKeyGenerator
import com.bitcointxoko.gudariwallet.util.NwcKeypair import com.bitcointxoko.gudariwallet.util.NwcKeypair
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -95,18 +97,14 @@ class NwcViewModel(
fun requestDelete(pubkey: String) { _pendingDeletePubkey.value = pubkey } fun requestDelete(pubkey: String) { _pendingDeletePubkey.value = pubkey }
fun cancelDelete() { _pendingDeletePubkey.value = null } fun cancelDelete() { _pendingDeletePubkey.value = null }
// confirmDelete() calls the existing deleteConnection() then clears the pending key // confirmDelete() calls the existing deleteConnection() then clears the pending key
fun confirmDelete() { fun confirmDelete(strings: AppStrings) {
_pendingDeletePubkey.value?.let { deleteConnection(it) } _pendingDeletePubkey.value?.let { deleteConnection(it, strings) }
_pendingDeletePubkey.value = null _pendingDeletePubkey.value = null
} }
init {
loadConnections(includeExpired = true)
}
// ── Load ────────────────────────────────────────────────────────────────── // ── Load ──────────────────────────────────────────────────────────────────
fun loadConnections(includeExpired: Boolean = false) { fun loadConnections(includeExpired: Boolean = false, strings: AppStrings) {
Timber.d("[$TAG] loadConnections(includeExpired=$includeExpired)") Timber.d("[$TAG] loadConnections(includeExpired=$includeExpired)")
isSyncing = true isSyncing = true
@@ -154,7 +152,7 @@ class NwcViewModel(
isSyncing = false isSyncing = false
if (_uiState.value !is NwcUiState.Success) { if (_uiState.value !is NwcUiState.Success) {
_uiState.value = NwcUiState.Error( _uiState.value = NwcUiState.Error(
e.message ?: "Failed to load connections" e.message ?: strings.nwc.failedToLoadConnections
) )
} else { } else {
Timber.w("[$TAG] network failed but cache is showing — non-fatal") Timber.w("[$TAG] network failed but cache is showing — non-fatal")
@@ -172,7 +170,8 @@ class NwcViewModel(
description : String, description : String,
permissions : List<String>, permissions : List<String>,
expiresAt : Long = 0L, expiresAt : Long = 0L,
budgets : List<NwcNewBudget> = emptyList() budgets : List<NwcNewBudget> = emptyList(),
strings : AppStrings
) { ) {
Timber.d("[$TAG] createConnection description='$description' permissions=$permissions") Timber.d("[$TAG] createConnection description='$description' permissions=$permissions")
viewModelScope.launch { viewModelScope.launch {
@@ -213,7 +212,7 @@ class NwcViewModel(
} }
.onFailure { e -> .onFailure { e ->
Timber.e(e, "[$TAG] createConnection failed") Timber.e(e, "[$TAG] createConnection failed")
_uiState.value = NwcUiState.Error(e.message ?: "Failed to create connection") _uiState.value = NwcUiState.Error(e.message ?: strings.nwc.failedToCreateConnection)
} }
} finally { } finally {
_isCreating.value = false _isCreating.value = false
@@ -234,7 +233,7 @@ class NwcViewModel(
* and the detail screen (revoke button). Sets [_isDeleting] for the duration * and the detail screen (revoke button). Sets [_isDeleting] for the duration
* so the list screen can disable row interactions while in flight. * so the list screen can disable row interactions while in flight.
*/ */
fun deleteConnection(pubkey: String) { fun deleteConnection(pubkey: String, strings: AppStrings) {
Timber.d("[$TAG] deleteConnection pubkey=${pubkey.take(16)}") Timber.d("[$TAG] deleteConnection pubkey=${pubkey.take(16)}")
viewModelScope.launch { viewModelScope.launch {
_isDeleting.value = true _isDeleting.value = true
@@ -254,7 +253,7 @@ class NwcViewModel(
.onFailure { e -> .onFailure { e ->
Timber.e(e, "[$TAG] failed to delete connection pubkey=${pubkey.take(16)}") Timber.e(e, "[$TAG] failed to delete connection pubkey=${pubkey.take(16)}")
_uiState.value = NwcUiState.Error( _uiState.value = NwcUiState.Error(
e.message ?: "Failed to delete connection" e.message ?: strings.nwc.failedToDeleteConnection
) )
} }
} finally { } finally {
@@ -264,7 +263,7 @@ class NwcViewModel(
} }
fun loadConnectionDetail(pubkey: String) { fun loadConnectionDetail(pubkey: String, strings: AppStrings) {
Timber.d("[$TAG] loadConnectionDetail pubkey=${pubkey.take(16)}") Timber.d("[$TAG] loadConnectionDetail pubkey=${pubkey.take(16)}")
// Cancel any subscription from a previous key — prevents stale Flow // Cancel any subscription from a previous key — prevents stale Flow
@@ -303,7 +302,7 @@ class NwcViewModel(
Timber.e(e, "[$TAG] failed to load connection detail") Timber.e(e, "[$TAG] failed to load connection detail")
if (_connectionDetailState.value !is ConnectionDetailState.Success) { if (_connectionDetailState.value !is ConnectionDetailState.Success) {
_connectionDetailState.value = ConnectionDetailState.Error( _connectionDetailState.value = ConnectionDetailState.Error(
e.message ?: "Failed to load connection" e.message ?: strings.nwc.failedToLoadConnection
) )
} else { } else {
Timber.w("[$TAG] detail network refresh failed — cache showing (non-fatal)") Timber.w("[$TAG] detail network refresh failed — cache showing (non-fatal)")
@@ -1,5 +1,7 @@
package com.bitcointxoko.gudariwallet.util package com.bitcointxoko.gudariwallet.util
import android.os.Build
import androidx.annotation.RequiresApi
import timber.log.Timber import timber.log.Timber
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
@@ -98,3 +100,15 @@ fun formatEpoch(epochSeconds: Long, todayLabel: String, yesterdayLabel: String):
"" ""
} }
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private fun epochToLocalDate(epochSeconds: Long): LocalDate =
LocalDate.ofInstant(Instant.ofEpochSecond(epochSeconds), ZoneId.systemDefault())
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
internal fun isToday(epochSeconds: Long): Boolean =
epochToLocalDate(epochSeconds) == LocalDate.now(ZoneId.systemDefault())
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
internal fun isYesterday(epochSeconds: Long): Boolean =
epochToLocalDate(epochSeconds) == LocalDate.now(ZoneId.systemDefault()).minusDays(1)