Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 181cf56239 | |||
| 262d5f068d | |||
| 201242c797 | |||
| 8c84eca8eb | |||
| 4e22f4d3e9 | |||
| c155f43870 |
Generated
+17
@@ -2,23 +2,40 @@
|
||||
<dictionary name="project">
|
||||
<words>
|
||||
<w>Aead</w>
|
||||
<w>Bech</w>
|
||||
<w>Bitrefill</w>
|
||||
<w>Español</w>
|
||||
<w>Nbits</w>
|
||||
<w>Nostr</w>
|
||||
<w>Tink</w>
|
||||
<w>amboss</w>
|
||||
<w>bech</w>
|
||||
<w>bitcointxoko</w>
|
||||
<w>btcpay</w>
|
||||
<w>cltv</w>
|
||||
<w>getnwckey</w>
|
||||
<w>gudari</w>
|
||||
<w>gudariwallet</w>
|
||||
<w>hkdf</w>
|
||||
<w>inflight</w>
|
||||
<w>keyauth</w>
|
||||
<w>lnaddr</w>
|
||||
<w>lnaddress</w>
|
||||
<w>lnbc</w>
|
||||
<w>lnbits</w>
|
||||
<w>lnurlp</w>
|
||||
<w>lnurlw</w>
|
||||
<w>msat</w>
|
||||
<w>msats</w>
|
||||
<w>nostr</w>
|
||||
<w>nwccacherepository</w>
|
||||
<w>satoshi</w>
|
||||
<w>satoshis</w>
|
||||
<w>sats</w>
|
||||
<w>snackbar</w>
|
||||
<w>snull</w>
|
||||
<w>tink</w>
|
||||
<w>walletconnect</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
@@ -2,24 +2,18 @@ package com.bitcointxoko.gudariwallet
|
||||
|
||||
import android.Manifest
|
||||
import android.app.PendingIntent
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.nfc.NfcAdapter
|
||||
import android.nfc.Tag
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
@@ -18,7 +18,7 @@ import kotlinx.serialization.json.jsonPrimitive
|
||||
* - a JSON array ["Thanks!"]
|
||||
* - null
|
||||
*
|
||||
* Mirrors the behaviour of the old Gson FlexibleStringAdapter.
|
||||
* Mirrors the behavior of the old Gson FlexibleStringAdapter.
|
||||
*/
|
||||
object FlexibleStringSerializer : KSerializer<String?> {
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.bitcointxoko.gudariwallet.api
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* Single source of truth for the app's Json instance.
|
||||
* Single source of truth for the app's JSON instance.
|
||||
*
|
||||
* All serialization/deserialization — Retrofit, DataStore caches,
|
||||
* and WebSocket message parsing — must use this instance so that
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.bitcointxoko.gudariwallet.api
|
||||
|
||||
import com.bitcointxoko.gudariwallet.security.SecretStore
|
||||
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -13,7 +12,6 @@ import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
class LNbitsClient(secrets: SecretStore) {
|
||||
@@ -38,8 +36,6 @@ class LNbitsClient(secrets: SecretStore) {
|
||||
}
|
||||
}
|
||||
|
||||
// Single shared client — used by both Retrofit and WebSocket
|
||||
val httpClient: OkHttpClient get() = LNbitsHttpClient.instance
|
||||
val api: LNbitsApi by lazy { buildApi() }
|
||||
|
||||
private fun buildApi(): LNbitsApi {
|
||||
|
||||
@@ -251,7 +251,7 @@ data class SuccessAction(
|
||||
* - a plain string "Thanks!"
|
||||
* - null
|
||||
*
|
||||
* kotlinx.serialization's default deserialiser throws when it sees a
|
||||
* kotlinx.serialization's default deserializer throws when it sees a
|
||||
* string where it expects {. This serializer handles all three cases.
|
||||
*
|
||||
* Applied via @Serializable(with = SuccessActionSerializer::class) on
|
||||
|
||||
@@ -337,10 +337,8 @@ class LNbitsWalletRepository(
|
||||
request : NwcRegistrationRequest
|
||||
): NwcGetResponse {
|
||||
Timber.d(
|
||||
"NWC [REGISTER ] pubkey=${pubkey.take(16)}… " +
|
||||
"permissions=${request.permissions} " +
|
||||
"expires_at=${request.expires_at} " +
|
||||
"budgets=${request.budgets.size}"
|
||||
"%snull", "NWC [REGISTER ] pubkey=${pubkey.take(16)}… " +
|
||||
"permissions=${request.permissions} "
|
||||
)
|
||||
val result = api.registerNwcKey(
|
||||
adminKey = secrets.adminKey(),
|
||||
|
||||
@@ -38,8 +38,7 @@ class LightningAddressStore(private val context: Context) {
|
||||
.map { prefs ->
|
||||
prefs[KEY_ADDRESS].also { address ->
|
||||
_currentAddress = address
|
||||
Timber.d("LNADDR [CACHE ] read from cache: " +
|
||||
if (address != null) "\"$address\"" else "(empty — nothing cached yet)") }
|
||||
Timber.d("LNADDR [CACHE ] read from cache: %s", if (address != null) "\"$address\"" else "(empty — nothing cached yet)") }
|
||||
}
|
||||
|
||||
suspend fun isCacheStale(): Boolean {
|
||||
@@ -53,9 +52,8 @@ class LightningAddressStore(private val context: Context) {
|
||||
val fetchedAt = prefs[KEY_FETCHED_AT] ?: 0L
|
||||
val age = System.currentTimeMillis() - fetchedAt
|
||||
val isStale = address == null || age > LNADDRESS_TTL_MS
|
||||
Timber.d("LNADDR [TTL ] cached=${address != null} " +
|
||||
"age=${age / 1000}s ttl=${LNADDRESS_TTL_MS / 1000}s " +
|
||||
if (isStale) "→ cache stale, will fetch from API"
|
||||
Timber.d("%s%s", "LNADDR [TTL ] cached=${address != null} " +
|
||||
"age=${age / 1000}s ttl=${LNADDRESS_TTL_MS / 1000}s ", if (isStale) "→ cache stale, will fetch from API"
|
||||
else "→ cache fresh, skipping API call")
|
||||
isStale
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class LnurlCacheRepository(context: Context) {
|
||||
return null
|
||||
}
|
||||
|
||||
val response = deserialise(entity.responseJson) ?: run {
|
||||
val response = deserialize(entity.responseJson) ?: run {
|
||||
Timber.w("LNURL [DESER ERR ] $key — removing corrupt entry")
|
||||
evict(key)
|
||||
return null
|
||||
@@ -104,7 +104,7 @@ class LnurlCacheRepository(context: Context) {
|
||||
runCatching { dao.deleteByKey(key) }
|
||||
}
|
||||
|
||||
private fun deserialise(jsonString: String): LnurlScanResponse? =
|
||||
private fun deserialize(jsonString: String): LnurlScanResponse? =
|
||||
runCatching { json.decodeFromString(LnurlScanResponse.serializer(), jsonString) }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.bitcointxoko.gudariwallet.i18n
|
||||
|
||||
import cafe.adriel.lyricist.LyricistStrings
|
||||
import kotlin.String
|
||||
|
||||
@LyricistStrings(languageTag = "en", default = true)
|
||||
val EnHomeStrings = AppStrings(
|
||||
@@ -55,6 +54,8 @@ val EnHomeStrings = AppStrings(
|
||||
details = "Details",
|
||||
enterValidAmount = "Enter a valid amount",
|
||||
satsUnit = { sats -> "$sats sats" },
|
||||
today = "today",
|
||||
yesterday = "yesterday",
|
||||
|
||||
// ── SendScreen — general ──────────────────────────────────────────────────
|
||||
send = "Send",
|
||||
@@ -377,6 +378,9 @@ val EnHomeStrings = AppStrings(
|
||||
connectionRowExpires = "Expires",
|
||||
connectionRowPubkey = "Public key",
|
||||
connectionNeverUsed = "Never",
|
||||
lastUsedToday = "Last used today",
|
||||
lastUsedYesterday = "Last used yesterday",
|
||||
lastUsedDate = "Last used %s",
|
||||
connectionNoExpiry = "Never",
|
||||
connectionExpired = { date -> "Expired · $date" },
|
||||
connectionSectionPermissions = "Permissions",
|
||||
@@ -398,6 +402,10 @@ val EnHomeStrings = AppStrings(
|
||||
permSignMessage = "Sign messages",
|
||||
permPay = "Send payments",
|
||||
permInvoice = "Create invoices",
|
||||
permLookup = "Lookup invoice status",
|
||||
permHistory = "Read transaction history",
|
||||
permBalance = "Read wallet balance",
|
||||
permInfo = "Read account info",
|
||||
permLookupHistory = "Lookup status & transaction history",
|
||||
permBalanceInfo = "Read balance & account info",
|
||||
permSendPayments = "Send payments",
|
||||
@@ -410,6 +418,57 @@ val EnHomeStrings = AppStrings(
|
||||
revokeDialogBody = "This will permanently revoke this Nostr Wallet Connect key. Any app using it will lose access.",
|
||||
revokeDialogConfirm = "Revoke",
|
||||
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",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.bitcointxoko.gudariwallet.i18n
|
||||
|
||||
import cafe.adriel.lyricist.LyricistStrings
|
||||
import com.bitcointxoko.gudariwallet.i18n.NwcStrings
|
||||
|
||||
@LyricistStrings(languageTag = "es")
|
||||
val EsHomeStrings = AppStrings(
|
||||
@@ -55,6 +54,8 @@ val EsHomeStrings = AppStrings(
|
||||
details = "Detalles",
|
||||
enterValidAmount = "Introduce una cantidad válida",
|
||||
satsUnit = { sats -> "$sats sats" },
|
||||
today = "hoy",
|
||||
yesterday = "ayer",
|
||||
|
||||
// ── SendScreen — general ──────────────────────────────────────────────────
|
||||
send = "Enviar",
|
||||
@@ -378,6 +379,9 @@ val EsHomeStrings = AppStrings(
|
||||
connectionRowExpires = "Vence",
|
||||
connectionRowPubkey = "Clave pública",
|
||||
connectionNeverUsed = "Nunca",
|
||||
lastUsedToday = "Usada hoy",
|
||||
lastUsedYesterday = "Usada ayer",
|
||||
lastUsedDate = "Usada %s",
|
||||
connectionNoExpiry = "Sin vencimiento",
|
||||
connectionExpired = { date -> "Vencido · $date" },
|
||||
connectionSectionPermissions = "Permisos",
|
||||
@@ -399,6 +403,10 @@ val EsHomeStrings = AppStrings(
|
||||
permSignMessage = "Firmar mensajes",
|
||||
permPay = "Enviar pagos",
|
||||
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",
|
||||
permBalanceInfo = "Ver saldo e información de cuenta",
|
||||
permSendPayments = "Enviar pagos",
|
||||
@@ -411,5 +419,56 @@ val EsHomeStrings = AppStrings(
|
||||
revokeDialogBody = "Esto revocará permanentemente esta clave Nostr Wallet Connect. Cualquier app que la use perderá el acceso.",
|
||||
revokeDialogConfirm = "Revocar",
|
||||
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",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -54,6 +54,8 @@ val EuStrings = AppStrings(
|
||||
details = "Xehetasunak",
|
||||
enterValidAmount = "Sartu zenbateko baliogarri bat",
|
||||
satsUnit = { sats -> "$sats sat" },
|
||||
today = "gaur",
|
||||
yesterday = "atzo",
|
||||
|
||||
// ── SendScreen — general ──────────────────────────────────────────────────
|
||||
send = "Bidali",
|
||||
@@ -374,6 +376,9 @@ val EuStrings = AppStrings(
|
||||
connectionSectionDetails = "Xehetasunak",
|
||||
connectionRowCreated = "Konektatuta",
|
||||
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",
|
||||
connectionRowPubkey = "Gako publikoa",
|
||||
connectionNeverUsed = "Inoiz ez",
|
||||
@@ -398,6 +403,10 @@ val EuStrings = AppStrings(
|
||||
permSignMessage = "Mezuak sinatu",
|
||||
permPay = "Ordainketak bidali",
|
||||
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",
|
||||
permBalanceInfo = "Saldoa eta kontuaren informazioa ikusi",
|
||||
permSendPayments = "Ordainketak bidali",
|
||||
@@ -410,5 +419,56 @@ val EuStrings = AppStrings(
|
||||
revokeDialogBody = "Honek Nostr Wallet Connect gako hau behin betiko baliogabetuko du. Erabiltzen duen edozein aplikaziok sarbidea galduko du.",
|
||||
revokeDialogConfirm = "Baliogabetu",
|
||||
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",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -63,6 +63,8 @@ data class AppStrings(
|
||||
val details : String,
|
||||
val enterValidAmount : String,
|
||||
val satsUnit : (sats: Long) -> String,
|
||||
val today : String,
|
||||
val yesterday : String,
|
||||
|
||||
// ── SendScreen — general ──────────────────────────────────────────────────
|
||||
val send : String,
|
||||
@@ -381,6 +383,9 @@ data class NwcStrings(
|
||||
val connectionRowExpires : String,
|
||||
val connectionRowPubkey : 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,
|
||||
/** "Expired · {date}" */
|
||||
val connectionExpired : (date: String) -> String,
|
||||
@@ -404,12 +409,16 @@ data class NwcStrings(
|
||||
val permLookupInvoice : String,
|
||||
val permListTransactions : String,
|
||||
val permSignMessage : String,
|
||||
val permPay : String, // "Send payments"
|
||||
val permInvoice : String, // "Create invoices"
|
||||
val permLookupHistory: String, // "Lookup status & transaction history"
|
||||
val permBalanceInfo : String, // "Read balance & account info"
|
||||
val permSendPayments: String,
|
||||
val permCreateInvoices: String,
|
||||
val permPay : String, // "Send payments"
|
||||
val permInvoice : String, // "Create invoices"
|
||||
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 permSendPayments : String,
|
||||
val permCreateInvoices : String,
|
||||
val permLookupInvoiceStatus: String,
|
||||
val permReadTransactionHistory: String,
|
||||
val permReadWalletBalance: String,
|
||||
@@ -418,4 +427,56 @@ data class NwcStrings(
|
||||
val revokeDialogBody : String,
|
||||
val revokeDialogConfirm : 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,
|
||||
)
|
||||
@@ -1,20 +1,15 @@
|
||||
package com.bitcointxoko.gudariwallet.security
|
||||
|
||||
import android.content.Context
|
||||
import timber.log.Timber
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.migrations.SharedPreferencesMigration
|
||||
import androidx.datastore.migrations.SharedPreferencesView
|
||||
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.io.File
|
||||
import androidx.core.content.edit
|
||||
|
||||
/**
|
||||
* Singleton DataStore for encrypted credentials.
|
||||
@@ -27,8 +22,6 @@ import androidx.core.content.edit
|
||||
* [cleanUpLegacyPrefs] once you've confirmed migration succeeded.
|
||||
*/
|
||||
object CredentialsDataStore {
|
||||
private const val TAG = "CredentialsDataStore"
|
||||
|
||||
@Volatile
|
||||
private var instance: DataStore<Credentials>? = null
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ class CredentialsSerializer(
|
||||
Timber.d("Credentials decrypted successfully — isOnboarded: ${result.isOnboarded}")
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Decryption failed — DataStore may be corrupt or key rotated", e)
|
||||
throw CorruptionException("Cannot read/decrypt credentials DataStore", e)
|
||||
Timber.e("Decryption failed — DataStore may be corrupt or key rotated")
|
||||
throw CorruptionException("Cannot read/decrypt credentials DataStore")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,12 +37,10 @@ class CredentialsSerializer(
|
||||
val encryptingStream = streamingAead.newEncryptingStream(output, AAD)
|
||||
t.writeTo(encryptingStream) // returns Int — discarded by Unit return type
|
||||
encryptingStream.close()
|
||||
Timber.d("Credentials encrypted and written — isOnboarded: ${t.isOnboarded}, " +
|
||||
"hasInvoiceKey: ${t.invoiceKey.isNotBlank()}, " +
|
||||
"hasAdminKey: ${t.adminKey.isNotBlank()}, " +
|
||||
"hasBaseUrl: ${t.baseUrl.isNotBlank()}")
|
||||
Timber.d("%snull", "Credentials encrypted and written — isOnboarded: ${t.isOnboarded}, " +
|
||||
"hasInvoiceKey: ${t.invoiceKey.isNotBlank()}, ")
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Encryption/write failed", e)
|
||||
Timber.e("Encryption/write failed")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ object CredentialsTinkManager {
|
||||
Timber.i("StreamingAead primitive acquired successfully")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Failed to initialise StreamingAead — Keystore may be unavailable", e)
|
||||
Timber.e("Failed to initialise StreamingAead — Keystore may be unavailable")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class NodeAliasService(
|
||||
repo.put(pubkey, alias)
|
||||
|
||||
// 4. Publish to StateFlow
|
||||
_aliases.value = _aliases.value + (pubkey to alias)
|
||||
_aliases.value += (pubkey to alias)
|
||||
|
||||
Timber.d("ALIAS [FETCHED ] $pubkey → \"$alias\"")
|
||||
return alias
|
||||
|
||||
+2
-1
@@ -30,6 +30,7 @@ import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
private const val TAG = "WalletNotifService"
|
||||
|
||||
@@ -271,7 +272,7 @@ class WalletNotificationService : Service() {
|
||||
|
||||
reconnectJob?.cancel()
|
||||
reconnectJob = serviceScope.launch {
|
||||
delay(backoff)
|
||||
delay(backoff.milliseconds)
|
||||
openWebSocket()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ import com.bitcointxoko.gudariwallet.ui.send.SendScreen
|
||||
import com.bitcointxoko.gudariwallet.ui.send.SendStrings
|
||||
import com.bitcointxoko.gudariwallet.util.SendInputType
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
// ── Tab destinations ──────────────────────────────────────────────────────────
|
||||
// Note: label and icon are now resolved at the call site via LocalAppStrings,
|
||||
@@ -205,7 +206,7 @@ fun WalletScreen(
|
||||
|
||||
LaunchedEffect(clipboardOffer) {
|
||||
if (clipboardOffer is ClipboardOfferState.Detected) {
|
||||
delay(8_000)
|
||||
delay(8_000.milliseconds)
|
||||
vm.dismissClipboardOffer()
|
||||
}
|
||||
}
|
||||
@@ -215,7 +216,7 @@ fun WalletScreen(
|
||||
|
||||
LaunchedEffect(nfcOffer) {
|
||||
if (nfcOffer is NfcOfferState.Detected) {
|
||||
delay(8_000)
|
||||
delay(8_000.milliseconds)
|
||||
nfcVm.dismissNfcOffer()
|
||||
}
|
||||
}
|
||||
@@ -460,7 +461,7 @@ fun WalletScreen(
|
||||
|
||||
val connectionState by nwcVm.connectionDetailState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(pubkey) { nwcVm.loadConnectionDetail(pubkey) }
|
||||
LaunchedEffect(pubkey) { nwcVm.loadConnectionDetail(pubkey, strings) }
|
||||
DisposableEffect(Unit) { onDispose { nwcVm.clearConnectionDetail() } }
|
||||
|
||||
when (val state = connectionState) {
|
||||
@@ -490,7 +491,7 @@ fun WalletScreen(
|
||||
budgets = state.budgets,
|
||||
onBack = { navController.popBackStack() },
|
||||
onRevoke = {
|
||||
nwcVm.deleteConnection(pubkey)
|
||||
nwcVm.deleteConnection(pubkey, strings)
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@ import com.bitcointxoko.gudariwallet.data.LightningAddressStore
|
||||
import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
|
||||
import com.bitcointxoko.gudariwallet.data.NwcCacheRepository
|
||||
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
||||
import com.bitcointxoko.gudariwallet.data.db.AppDatabase
|
||||
import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore
|
||||
import com.bitcointxoko.gudariwallet.service.NodeAliasService
|
||||
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
||||
|
||||
@@ -75,11 +75,10 @@ class BalanceViewModel(
|
||||
runCatching { repo.getBalance() }
|
||||
.onSuccess { balance ->
|
||||
val sats = balance.sats
|
||||
val prev = (_balanceState.value as? BalanceState.Success)?.sats
|
||||
when {
|
||||
prev == null -> Timber.d("BALANCE [NETWORK ] $sats sats — cold load complete")
|
||||
prev == sats -> Timber.d("BALANCE [NETWORK ] $sats sats — matches cache, no change")
|
||||
else -> Timber.d("BALANCE [NETWORK ] $sats sats — was $prev sats (diff ${sats - prev})")
|
||||
when (val prev = (_balanceState.value as? BalanceState.Success)?.sats) {
|
||||
null -> Timber.d("BALANCE [NETWORK ] $sats sats — cold load complete")
|
||||
sats -> Timber.d("BALANCE [NETWORK ] $sats sats — matches cache, no change")
|
||||
else -> Timber.d("BALANCE [NETWORK ] $sats sats — was $prev sats (diff ${sats - prev})")
|
||||
}
|
||||
_balanceState.value = BalanceState.Success(
|
||||
sats = sats,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.bitcointxoko.gudariwallet.ui.common
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
/**
|
||||
* Manages screen brightness for QR-display screens.
|
||||
@@ -12,12 +11,12 @@ import androidx.compose.ui.platform.LocalContext
|
||||
*/
|
||||
@Composable
|
||||
fun rememberBrightnessController(): BrightnessController {
|
||||
val window = (LocalContext.current as? Activity)?.window
|
||||
val window = (LocalActivity.current)?.window
|
||||
var isOn by remember { mutableStateOf(false) }
|
||||
|
||||
// Sync side-effect: no coroutine needed
|
||||
// Sync side effect: no coroutine needed
|
||||
SideEffect {
|
||||
window?.attributes = window?.attributes?.apply {
|
||||
window?.attributes = window.attributes?.apply {
|
||||
screenBrightness = if (isOn) WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL
|
||||
else WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||
}
|
||||
@@ -25,7 +24,7 @@ fun rememberBrightnessController(): BrightnessController {
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
window?.attributes = window?.attributes?.apply {
|
||||
window?.attributes = window.attributes?.apply {
|
||||
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import com.bitcointxoko.gudariwallet.util.satsToFiat
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -220,7 +220,7 @@ internal fun FilterBottomSheet(
|
||||
) {
|
||||
Text(
|
||||
currentFilter.minCreatedAt
|
||||
?.let { formatEpochShort(it) }
|
||||
?.let { formatEpochShort(it, strings.today, strings.yesterday) }
|
||||
?: strings.history.filterDateFrom_label // ← "From"
|
||||
)
|
||||
}
|
||||
@@ -231,7 +231,7 @@ internal fun FilterBottomSheet(
|
||||
) {
|
||||
Text(
|
||||
currentFilter.maxCreatedAt
|
||||
?.let { formatEpochShort(it) }
|
||||
?.let { formatEpochShort(it, strings.today, strings.yesterday) }
|
||||
?: strings.history.filterDateTo_label // ← "To"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -464,13 +464,13 @@ private fun ActiveFilterChips(
|
||||
null -> when {
|
||||
minCreatedAt != null && maxCreatedAt != null ->
|
||||
strings.history.filterDateRange( // ← "X–Y"
|
||||
formatEpochShort(minCreatedAt),
|
||||
formatEpochShort(maxCreatedAt)
|
||||
formatEpochShort(minCreatedAt, strings.today, strings.yesterday),
|
||||
formatEpochShort(maxCreatedAt, strings.today, strings.yesterday)
|
||||
)
|
||||
minCreatedAt != null ->
|
||||
strings.history.filterDateFrom(formatEpochShort(minCreatedAt)) // ← "From X"
|
||||
strings.history.filterDateFrom(formatEpochShort(minCreatedAt, strings.today, strings.yesterday)) // ← "From X"
|
||||
else ->
|
||||
strings.history.filterDateUntil(formatEpochShort(maxCreatedAt!!)) // ← "Until X"
|
||||
strings.history.filterDateUntil(formatEpochShort(maxCreatedAt!!, strings.today, strings.yesterday)) // ← "Until X"
|
||||
}
|
||||
}
|
||||
InputChip(
|
||||
|
||||
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
private const val TAG = "HistoryViewModel"
|
||||
|
||||
@@ -156,7 +157,7 @@ class HistoryViewModel(
|
||||
|
||||
viewModelScope.launch {
|
||||
combine(
|
||||
_filter.map { it.searchQuery }.debounce(300),
|
||||
_filter.map { it.searchQuery }.debounce(300.milliseconds),
|
||||
_filter
|
||||
) { _, f -> f }
|
||||
.collect { f ->
|
||||
@@ -164,10 +165,8 @@ class HistoryViewModel(
|
||||
Timber.d("FILTER [IN-MEMORY] filter=default → using cached state")
|
||||
_roomResults.value = null
|
||||
} else {
|
||||
Timber.d("FILTER [ROOM ] q=\"${f.searchQuery}\" " +
|
||||
"statuses=${f.statuses} dir=${f.direction} types=${f.types} " +
|
||||
"amt=${f.minAmountSat}–${f.maxAmountSat} " +
|
||||
"date=${f.minCreatedAt}–${f.maxCreatedAt}")
|
||||
Timber.d("%snull", "FILTER [ROOM ] q=\"${f.searchQuery}\" " +
|
||||
"statuses=${f.statuses} dir=${f.direction} types=${f.types} ")
|
||||
_roomResults.value = paymentCache.queryAll(f.searchQuery, f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -234,7 +233,11 @@ private fun PaymentDetailContent(
|
||||
// ── Basic info ───────────────────────────────────────────────────────
|
||||
item {
|
||||
DetailSection(title = strings.history.detailSectionDetails) { // ← "Details"
|
||||
DetailRow(strings.history.detailRowDate, formatTimestamp(payment.createdAt, payment.time)) // ← "Date"
|
||||
DetailRow(strings.history.detailRowDate, formatTimestamp( // ← "Date"
|
||||
payment.createdAt,
|
||||
payment.time,
|
||||
strings.today,
|
||||
strings.yesterday))
|
||||
DetailRow(
|
||||
strings.history.detailRowMemo, // ← "Memo"
|
||||
payment.memo?.takeIf { it.isNotBlank() } ?: strings.history.detailRowMemoEmpty // ← "—"
|
||||
|
||||
@@ -78,7 +78,7 @@ internal fun PaymentRow(
|
||||
)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
val formattedTime = remember(payment.createdAt ?: payment.time) {
|
||||
formatTimestamp(payment.createdAt, payment.time)
|
||||
formatTimestamp(payment.createdAt, payment.time, strings.today, strings.yesterday)
|
||||
}
|
||||
Text(
|
||||
text = formattedTime,
|
||||
|
||||
@@ -159,8 +159,7 @@ class PaymentSyncManager(
|
||||
val reachedEnd = page.size < WalletConstants.PAYMENTS_PAGE_SIZE
|
||||
|
||||
if (reachedOverlap || reachedEnd) {
|
||||
Timber.d("PAYMENTS [SYNC DONE ] $totalNew new payment(s) across $pagesChecked page(s) — " +
|
||||
if (reachedOverlap) "stopped at known record" else "reached end of history")
|
||||
Timber.d("null%s", if (reachedOverlap) "stopped at known record" else "reached end of history")
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -77,13 +77,12 @@ class NdefHceService : HostApduService() {
|
||||
|
||||
val ins = apdu[1]
|
||||
|
||||
return when {
|
||||
return when (ins) {
|
||||
// ── SELECT (INS = 0xA4) ──────────────────────────────────────────
|
||||
ins == 0xA4.toByte() -> handleSelect(apdu)
|
||||
0xA4.toByte() -> handleSelect(apdu)
|
||||
|
||||
// ── READ BINARY (INS = 0xB0) ─────────────────────────────────────
|
||||
ins == 0xB0.toByte() -> handleReadBinary(apdu)
|
||||
|
||||
0xB0.toByte() -> handleReadBinary(apdu)
|
||||
else -> SW_UNKNOWN
|
||||
}.also { Timber.d("APDU OUT: ${it.toHex()}") }
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitcointxoko.gudariwallet.ui.NfcOfferState
|
||||
import com.bitcointxoko.gudariwallet.util.SendInputDetector
|
||||
import com.bitcointxoko.gudariwallet.util.SendInputType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -83,7 +82,7 @@ class NfcViewModel : ViewModel() {
|
||||
intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
|
||||
}
|
||||
|
||||
if (rawMessages != null && rawMessages.isNotEmpty()) {
|
||||
if (!rawMessages.isNullOrEmpty()) {
|
||||
val message = rawMessages[0] as NdefMessage
|
||||
val uri = extractUri(message)
|
||||
if (uri != null) {
|
||||
@@ -98,7 +97,7 @@ class NfcViewModel : ViewModel() {
|
||||
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
|
||||
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
|
||||
}
|
||||
if (tag != null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
|
||||
@@ -16,6 +16,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitcointxoko.gudariwallet.api.NwcNewBudget
|
||||
import com.bitcointxoko.gudariwallet.i18n.AppStrings
|
||||
import com.bitcointxoko.gudariwallet.strings
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
@@ -41,6 +43,16 @@ private class AddConnectionState {
|
||||
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 ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -58,6 +70,7 @@ internal fun NwcAddConnectionSheet(
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val state = remember { AddConnectionState() }
|
||||
|
||||
|
||||
if (state.showDatePicker) {
|
||||
val initialMillis = state.expiry
|
||||
.atZone(ZoneId.systemDefault())
|
||||
@@ -81,10 +94,10 @@ internal fun NwcAddConnectionSheet(
|
||||
.withDayOfMonth(picked.dayOfMonth)
|
||||
}
|
||||
state.showDatePicker = false
|
||||
}) { Text("OK") }
|
||||
}) { Text(strings.nwc.ok) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { state.showDatePicker = false }) { Text("Cancel") }
|
||||
TextButton(onClick = { state.showDatePicker = false }) { Text(strings.nwc.cancel) }
|
||||
}
|
||||
) { DatePicker(state = datePickerState) }
|
||||
}
|
||||
@@ -105,10 +118,10 @@ internal fun NwcAddConnectionSheet(
|
||||
.withSecond(0)
|
||||
.withNano(0)
|
||||
state.showTimePicker = false
|
||||
}) { Text("OK") }
|
||||
}) { Text(strings.nwc.ok) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { state.showTimePicker = false }) { Text("Cancel") }
|
||||
TextButton(onClick = { state.showTimePicker = false }) { Text(strings.nwc.cancel) }
|
||||
},
|
||||
text = { TimePicker(state = timePickerState) }
|
||||
)
|
||||
@@ -127,7 +140,7 @@ internal fun NwcAddConnectionSheet(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "New connection",
|
||||
text = strings.nwc.newConnection,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
@@ -135,7 +148,7 @@ internal fun NwcAddConnectionSheet(
|
||||
OutlinedTextField(
|
||||
value = state.description,
|
||||
onValueChange = { state.description = it },
|
||||
label = { Text("Label (e.g. Amethyst, Bitrefill)") },
|
||||
label = { Text(strings.nwc.labelHint) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
@@ -145,7 +158,7 @@ internal fun NwcAddConnectionSheet(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(text = "Expires", style = MaterialTheme.typography.bodyMedium)
|
||||
Text(text = strings.nwc.expires, style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
if (!state.neverExpires) {
|
||||
@@ -158,7 +171,7 @@ internal fun NwcAddConnectionSheet(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CalendarToday,
|
||||
contentDescription = "Pick date",
|
||||
contentDescription = strings.nwc.pickDate,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
@@ -173,7 +186,7 @@ internal fun NwcAddConnectionSheet(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Schedule,
|
||||
contentDescription = "Pick time",
|
||||
contentDescription = strings.nwc.pickTime,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
@@ -185,7 +198,7 @@ internal fun NwcAddConnectionSheet(
|
||||
checked = state.neverExpires,
|
||||
onCheckedChange = { state.neverExpires = it }
|
||||
)
|
||||
Text(text = "Never", style = MaterialTheme.typography.bodyMedium)
|
||||
Text(text = strings.nwc.never, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
|
||||
Row(
|
||||
@@ -193,7 +206,7 @@ internal fun NwcAddConnectionSheet(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Budgets",
|
||||
text = strings.nwc.budgets,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -206,11 +219,11 @@ internal fun NwcAddConnectionSheet(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = "Add budget",
|
||||
contentDescription = strings.nwc.addBudget,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Add budget")
|
||||
Text(strings.nwc.addBudget)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +243,7 @@ internal fun NwcAddConnectionSheet(
|
||||
|
||||
// ── Permissions ──────────────────────────────────────────────────
|
||||
Text(
|
||||
text = "Permissions",
|
||||
text = strings.nwc.permissions,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -251,11 +264,11 @@ internal fun NwcAddConnectionSheet(
|
||||
}
|
||||
)
|
||||
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 ─────────────────────────────────────────────────
|
||||
Button(
|
||||
onClick = {
|
||||
@@ -263,7 +276,7 @@ internal fun NwcAddConnectionSheet(
|
||||
else state.expiryEpochSeconds() // #10
|
||||
val validBudgets = state.budgets.mapNotNull { it.toNwcNewBudget() }
|
||||
onCreate(
|
||||
state.description.trim().ifBlank { "Unnamed" },
|
||||
state.description.trim().ifBlank { unnamed },
|
||||
state.selectedPermissions.toList(),
|
||||
expiresAtSecs,
|
||||
validBudgets
|
||||
@@ -280,7 +293,7 @@ internal fun NwcAddConnectionSheet(
|
||||
)
|
||||
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 ────────────────────────────────────────────────────────────────
|
||||
|
||||
@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)
|
||||
@Composable
|
||||
private fun BudgetRow(
|
||||
@@ -313,7 +335,7 @@ private fun BudgetRow(
|
||||
OutlinedTextField(
|
||||
value = draft.amountSats,
|
||||
onValueChange = { onChange(draft.copy(amountSats = it.filter(Char::isDigit))) },
|
||||
label = { Text("Sats") },
|
||||
label = { Text(strings.nwc.sats) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.weight(1f).height(fieldHeight)
|
||||
@@ -325,10 +347,10 @@ private fun BudgetRow(
|
||||
modifier = Modifier.weight(1f).height(fieldHeight)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = draft.refreshWindow.label,
|
||||
value = draft.refreshWindow.displayLabel(strings),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Resets") },
|
||||
label = { Text(strings.nwc.resets) },
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = dropdownExpanded)
|
||||
},
|
||||
@@ -343,7 +365,7 @@ private fun BudgetRow(
|
||||
) {
|
||||
BudgetRefreshWindow.entries.forEach { window ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(window.label) },
|
||||
text = { Text(window.displayLabel(strings)) },
|
||||
onClick = {
|
||||
onChange(draft.copy(refreshWindow = window))
|
||||
dropdownExpanded = false
|
||||
@@ -359,7 +381,7 @@ private fun BudgetRow(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Remove budget",
|
||||
contentDescription = strings.nwc.removeBudget,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.bitcointxoko.gudariwallet.ui.nwc
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -14,18 +16,22 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitcointxoko.gudariwallet.api.NwcGetResponse
|
||||
import com.bitcointxoko.gudariwallet.strings
|
||||
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
|
||||
import com.bitcointxoko.gudariwallet.util.formatEpoch
|
||||
import com.bitcointxoko.gudariwallet.util.formatEpochShort
|
||||
import com.bitcointxoko.gudariwallet.util.isToday
|
||||
import com.bitcointxoko.gudariwallet.util.isYesterday
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
@Composable
|
||||
internal fun NwcConnectionRow(
|
||||
connection : NwcGetResponse,
|
||||
isDeleting : Boolean,
|
||||
onDeleteConfirm : () -> Unit,
|
||||
connection: NwcGetResponse,
|
||||
isDeleting: Boolean,
|
||||
onDeleteConfirm: () -> Unit,
|
||||
onConnectionClick: () -> Unit,
|
||||
modifier : Modifier = Modifier
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val key = connection.data
|
||||
val expired = key.expires_at > 0 && key.expires_at < Instant.now().epochSecond
|
||||
@@ -35,7 +41,7 @@ internal fun NwcConnectionRow(
|
||||
val dismissState = rememberSwipeToDismissBoxState(
|
||||
confirmValueChange = {
|
||||
// Always reject — we never want the row to stay dismissed.
|
||||
// The dialog is triggered via LaunchedEffect on targetValue below.
|
||||
// The dialogue is triggered via LaunchedEffect on targetValue below.
|
||||
false
|
||||
},
|
||||
positionalThreshold = { totalDistance -> totalDistance * 0.4f }
|
||||
@@ -47,7 +53,7 @@ internal fun NwcConnectionRow(
|
||||
var dialogTriggered by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// Fires when the swipe crosses the threshold — trigger confirm dialog
|
||||
// Fires when the swipe crosses the threshold — trigger confirm dialogue
|
||||
LaunchedEffect(dismissState.targetValue) {
|
||||
if (dismissState.targetValue == SwipeToDismissBoxValue.EndToStart
|
||||
&& !isDeleting
|
||||
@@ -84,7 +90,7 @@ internal fun NwcConnectionRow(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Delete connection",
|
||||
contentDescription = strings.nwc.deleteConnection,
|
||||
tint = MaterialTheme.colorScheme.onError.copy(alpha = bgAlpha)
|
||||
)
|
||||
}
|
||||
@@ -110,7 +116,7 @@ internal fun NwcConnectionRow(
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = key.description.ifBlank { "Unnamed connection" },
|
||||
text = key.description.ifBlank { strings.nwc.unnamedConnection },
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
@@ -120,9 +126,15 @@ internal fun NwcConnectionRow(
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
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 = if (key.last_used <= 0L) "Never used"
|
||||
else "Last used ${formatEpoch(key.last_used)}",
|
||||
text = if (key.last_used <= 0L) strings.nwc.neverUsed
|
||||
else lastUsedText,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -134,7 +146,7 @@ internal fun NwcConnectionRow(
|
||||
contentColor = semantic.onErrorContainer
|
||||
) {
|
||||
Text(
|
||||
text = "Expired",
|
||||
text = strings.nwc.expired,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||
)
|
||||
|
||||
@@ -92,8 +92,6 @@ fun ConnectionDetailScreen(
|
||||
onRevoke : (() -> Unit)? = null // optional – show Revoke button when provided
|
||||
) {
|
||||
val strings = LocalAppStrings.current
|
||||
val clipboard = LocalClipboard.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -218,13 +216,13 @@ private fun ConnectionDetailContent(
|
||||
|
||||
DetailRow(
|
||||
label = strings.nwc.connectionRowCreated, // ← "Connected"
|
||||
value = formatEpoch(connection.createdAt)
|
||||
value = formatEpoch(connection.createdAt, strings.today, strings.yesterday)
|
||||
)
|
||||
|
||||
DetailRow(
|
||||
label = strings.nwc.connectionRowLastUsed, // ← "Last used"
|
||||
value = if (connection.lastUsed > 0)
|
||||
formatEpoch(connection.lastUsed)
|
||||
formatEpoch(connection.lastUsed, strings.today, strings.yesterday)
|
||||
else
|
||||
strings.nwc.connectionNeverUsed // ← "Never"
|
||||
)
|
||||
@@ -234,9 +232,13 @@ private fun ConnectionDetailContent(
|
||||
value = when {
|
||||
connection.expiresAt <= 0 -> strings.nwc.connectionNoExpiry // ← "Never"
|
||||
isExpired -> strings.nwc.connectionExpired( // ← "Expired · 12 Jan 2025"
|
||||
formatEpoch(connection.expiresAt)
|
||||
formatEpoch(connection.expiresAt, strings.today, strings.yesterday)
|
||||
)
|
||||
else -> formatEpoch(
|
||||
connection.expiresAt,
|
||||
strings.today,
|
||||
strings.yesterday
|
||||
)
|
||||
else -> formatEpoch(connection.expiresAt)
|
||||
},
|
||||
// valueColor = if (isExpired) MaterialTheme.colorScheme.error else null
|
||||
)
|
||||
@@ -383,7 +385,7 @@ private fun BudgetCard(budget: BudgetData) {
|
||||
// Created at
|
||||
DetailRow(
|
||||
label = strings.nwc.connectionRowCreated, // ← "Created"
|
||||
value = formatEpoch(budget.createdAt)
|
||||
value = formatEpoch(budget.createdAt, strings.today, strings.yesterday)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -415,7 +417,7 @@ private fun PermissionRow(permission: ConnectionPermission, strings: AppStrings)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = permission.label(strings), // already localised
|
||||
text = permission.label(strings), // already localized
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
@@ -5,17 +5,16 @@ import com.bitcointxoko.gudariwallet.api.NwcNewBudget
|
||||
// ── Available permissions ────────────────────────────────────────────
|
||||
internal data class NwcPermission(
|
||||
val serverKey : String, // what we send in the permissions array
|
||||
val displayLabel : String, // shown in the UI
|
||||
val default : Boolean // pre-checked by default
|
||||
)
|
||||
|
||||
internal val ALL_PERMISSIONS = listOf(
|
||||
NwcPermission("pay", "Send payments", default = true),
|
||||
NwcPermission("invoice", "Create invoices", default = false),
|
||||
NwcPermission("lookup", "Lookup invoice status", default = false),
|
||||
NwcPermission("history", "Read transaction history", default = false),
|
||||
NwcPermission("balance", "Read wallet balance", default = false),
|
||||
NwcPermission("info", "Read account info", default = false)
|
||||
NwcPermission("pay", default = true),
|
||||
NwcPermission("invoice", default = false),
|
||||
NwcPermission("lookup", default = false),
|
||||
NwcPermission("history", default = false),
|
||||
NwcPermission("balance", default = false),
|
||||
NwcPermission("info", default = false)
|
||||
)
|
||||
|
||||
// ── Budget ────────────────────────────────────────────────────────────
|
||||
@@ -24,12 +23,12 @@ internal val ALL_PERMISSIONS = listOf(
|
||||
* 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);
|
||||
enum class BudgetRefreshWindow(val seconds: Int) {
|
||||
DAILY (86_400),
|
||||
WEEKLY (604_800),
|
||||
MONTHLY (2_592_000),
|
||||
YEARLY (31_536_000),
|
||||
NEVER (0);
|
||||
}
|
||||
|
||||
data class BudgetDraft(
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import com.bitcointxoko.gudariwallet.strings
|
||||
import com.bitcointxoko.gudariwallet.ui.common.rememberBrightnessController
|
||||
import com.bitcointxoko.gudariwallet.ui.common.QrDisplayCard
|
||||
|
||||
@@ -29,20 +30,21 @@ internal fun NwcPairingUrlDialog(
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Connection created") },
|
||||
title = { Text(strings.nwc.connectionCreated) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
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,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
val strings = strings
|
||||
QrDisplayCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
content = url,
|
||||
contentDescription = "NWC pairing QR code",
|
||||
clipLabel = "NWC connection string",
|
||||
shareTitle = "Share NWC connection string",
|
||||
contentDescription = strings.nwc.nwcPairingQrCode,
|
||||
clipLabel = strings.nwc.nwcConnectionString,
|
||||
shareTitle = strings.nwc.shareNwcConnectionString,
|
||||
textToCopy = url,
|
||||
brightnessOn = brightness.isOn,
|
||||
onToggleBrightness = brightness.toggle,
|
||||
@@ -52,7 +54,7 @@ internal fun NwcPairingUrlDialog(
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
try {
|
||||
context.startActivity(
|
||||
Intent.createChooser(intent, "Open in app")
|
||||
Intent.createChooser(intent, strings.nwc.openInApp)
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
onNoAppFound() // #17: no Toast — caller handles it
|
||||
@@ -63,7 +65,7 @@ internal fun NwcPairingUrlDialog(
|
||||
},
|
||||
confirmButton = {}, // actions live inside QrDisplayCard
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text("Done") }
|
||||
TextButton(onClick = onDismiss) { Text(strings.nwc.done) }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.bitcointxoko.gudariwallet.ui.nwc
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -13,8 +15,10 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NwcScreen(
|
||||
@@ -30,8 +34,13 @@ fun NwcScreen(
|
||||
val pendingDeletePubkey by viewModel.pendingDeletePubkey.collectAsStateWithLifecycle()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
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) {
|
||||
if (pairingUrl != null) {
|
||||
@@ -42,17 +51,17 @@ fun NwcScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Wallet Connect") },
|
||||
title = { Text(strings.nwc.walletConnect) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = strings.nwc.back)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = { viewModel.openAddSheet() }) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Add connection")
|
||||
Icon(Icons.Default.Add, contentDescription = strings.nwc.addConnection)
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
@@ -102,7 +111,7 @@ fun NwcScreen(
|
||||
connection,
|
||||
isDeleting,
|
||||
onDeleteConfirm = { viewModel.requestDelete(connection.data.pubkey) },
|
||||
onConnectionClick = { onConnectionClick(connection.data.pubkey) }
|
||||
{ onConnectionClick(connection.data.pubkey) }
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
@@ -125,7 +134,8 @@ fun NwcScreen(
|
||||
description = description,
|
||||
permissions = permissions,
|
||||
expiresAt = expiresAt,
|
||||
budgets = budgets
|
||||
budgets = budgets,
|
||||
strings = strings
|
||||
)
|
||||
},
|
||||
onDismiss = { viewModel.closeAddSheet() }
|
||||
@@ -134,12 +144,13 @@ fun NwcScreen(
|
||||
|
||||
// ── Pairing URL dialog ─────────────────────────────────────────────────────
|
||||
pairingUrl?.let { url ->
|
||||
val strings = strings
|
||||
NwcPairingUrlDialog(
|
||||
url = url,
|
||||
onDismiss = { viewModel.clearPairingUrl() },
|
||||
onNoAppFound = {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar("No compatible app found")
|
||||
snackbarHostState.showSnackbar(strings.nwc.noCompatibleAppFound)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -149,15 +160,15 @@ fun NwcScreen(
|
||||
pendingDeletePubkey?.let {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.cancelDelete() },
|
||||
title = { Text("Remove connection?") },
|
||||
text = { Text("This will permanently revoke this Nostr Wallet Connect key. Any app using it will lose access.") },
|
||||
title = { Text(strings.nwc.removeConnection) },
|
||||
text = { Text(strings.nwc.removeConnectionDetail) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { viewModel.confirmDelete() }
|
||||
) { Text("Remove", color = MaterialTheme.colorScheme.error) }
|
||||
onClick = { viewModel.confirmDelete(strings) }
|
||||
) { Text(strings.nwc.remove, color = MaterialTheme.colorScheme.error) }
|
||||
},
|
||||
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.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitcointxoko.gudariwallet.strings
|
||||
|
||||
// ── Empty state ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -21,10 +22,10 @@ internal fun NwcEmptyContent(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text("No connections yet", style = MaterialTheme.typography.titleMedium)
|
||||
Text(strings.nwc.noConnectionsYet, style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
"Tap + to connect an app via Nostr Wallet Connect.",
|
||||
strings.nwc.tapToAddHint,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -32,7 +33,7 @@ internal fun NwcEmptyContent(
|
||||
Button(onClick = onAddNew) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Add connection")
|
||||
Text(strings.nwc.addConnection)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +52,7 @@ internal fun NwcErrorContent(
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
"Something went wrong",
|
||||
strings.nwc.somethingWentWrong,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
@@ -62,6 +63,6 @@ internal fun NwcErrorContent(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
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.ViewModelProvider
|
||||
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.NwcNewBudget
|
||||
import com.bitcointxoko.gudariwallet.api.NwcRegistrationRequest
|
||||
import com.bitcointxoko.gudariwallet.data.NwcCacheRepository
|
||||
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
||||
import com.bitcointxoko.gudariwallet.data.db.toDomain
|
||||
import com.bitcointxoko.gudariwallet.data.toDomain
|
||||
import com.bitcointxoko.gudariwallet.data.toGetResponse
|
||||
import com.bitcointxoko.gudariwallet.i18n.AppStrings
|
||||
import com.bitcointxoko.gudariwallet.util.NwcKeyGenerator
|
||||
import com.bitcointxoko.gudariwallet.util.NwcKeypair
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -95,18 +97,14 @@ class NwcViewModel(
|
||||
fun requestDelete(pubkey: String) { _pendingDeletePubkey.value = pubkey }
|
||||
fun cancelDelete() { _pendingDeletePubkey.value = null }
|
||||
// confirmDelete() calls the existing deleteConnection() then clears the pending key
|
||||
fun confirmDelete() {
|
||||
_pendingDeletePubkey.value?.let { deleteConnection(it) }
|
||||
fun confirmDelete(strings: AppStrings) {
|
||||
_pendingDeletePubkey.value?.let { deleteConnection(it, strings) }
|
||||
_pendingDeletePubkey.value = null
|
||||
}
|
||||
|
||||
init {
|
||||
loadConnections(includeExpired = true)
|
||||
}
|
||||
|
||||
// ── Load ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fun loadConnections(includeExpired: Boolean = false) {
|
||||
fun loadConnections(includeExpired: Boolean = false, strings: AppStrings) {
|
||||
Timber.d("[$TAG] loadConnections(includeExpired=$includeExpired)")
|
||||
isSyncing = true
|
||||
|
||||
@@ -154,7 +152,7 @@ class NwcViewModel(
|
||||
isSyncing = false
|
||||
if (_uiState.value !is NwcUiState.Success) {
|
||||
_uiState.value = NwcUiState.Error(
|
||||
e.message ?: "Failed to load connections"
|
||||
e.message ?: strings.nwc.failedToLoadConnections
|
||||
)
|
||||
} else {
|
||||
Timber.w("[$TAG] network failed but cache is showing — non-fatal")
|
||||
@@ -172,7 +170,8 @@ class NwcViewModel(
|
||||
description : String,
|
||||
permissions : List<String>,
|
||||
expiresAt : Long = 0L,
|
||||
budgets : List<NwcNewBudget> = emptyList()
|
||||
budgets : List<NwcNewBudget> = emptyList(),
|
||||
strings : AppStrings
|
||||
) {
|
||||
Timber.d("[$TAG] createConnection description='$description' permissions=$permissions")
|
||||
viewModelScope.launch {
|
||||
@@ -213,7 +212,7 @@ class NwcViewModel(
|
||||
}
|
||||
.onFailure { e ->
|
||||
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 {
|
||||
_isCreating.value = false
|
||||
@@ -234,7 +233,7 @@ class NwcViewModel(
|
||||
* and the detail screen (revoke button). Sets [_isDeleting] for the duration
|
||||
* 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)}…")
|
||||
viewModelScope.launch {
|
||||
_isDeleting.value = true
|
||||
@@ -254,7 +253,7 @@ class NwcViewModel(
|
||||
.onFailure { e ->
|
||||
Timber.e(e, "[$TAG] failed to delete connection pubkey=${pubkey.take(16)}…")
|
||||
_uiState.value = NwcUiState.Error(
|
||||
e.message ?: "Failed to delete connection"
|
||||
e.message ?: strings.nwc.failedToDeleteConnection
|
||||
)
|
||||
}
|
||||
} 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)}…")
|
||||
|
||||
// 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")
|
||||
if (_connectionDetailState.value !is ConnectionDetailState.Success) {
|
||||
_connectionDetailState.value = ConnectionDetailState.Error(
|
||||
e.message ?: "Failed to load connection"
|
||||
e.message ?: strings.nwc.failedToLoadConnection
|
||||
)
|
||||
} else {
|
||||
Timber.w("[$TAG] detail network refresh failed — cache showing (non-fatal)")
|
||||
|
||||
@@ -265,7 +265,7 @@ fun NotificationsPage(
|
||||
imageVector = Icons.Default.Notifications,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(72.dp),
|
||||
// Mirror the BatteryPage tint behaviour
|
||||
// Mirror the BatteryPage tint behavior
|
||||
tint = if (alreadyGranted)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
@@ -300,7 +300,7 @@ fun NotificationsPage(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Page 5: Battery optimisation ──────────────────────────────────────────────
|
||||
// ── Page 5: Battery optimization ──────────────────────────────────────────────
|
||||
@Composable
|
||||
fun BatteryPage(strings: AppStrings) {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -13,11 +13,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.work.WorkInfo
|
||||
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||
import com.bitcointxoko.gudariwallet.data.HistoricalSyncStore
|
||||
import com.bitcointxoko.gudariwallet.security.SecretStore
|
||||
import com.bitcointxoko.gudariwallet.sync.HistoricalSyncWorker
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.bitcointxoko.gudariwallet.ui.receive
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BrightnessHigh
|
||||
import androidx.compose.material.icons.filled.BrightnessLow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||
|
||||
@Composable
|
||||
internal fun BrightnessToggleButton(
|
||||
isOn : Boolean,
|
||||
onToggle: () -> Unit
|
||||
) {
|
||||
val strings = LocalAppStrings.current
|
||||
|
||||
IconButton(onClick = onToggle) {
|
||||
Icon(
|
||||
imageVector = if (isOn) Icons.Filled.BrightnessHigh
|
||||
else Icons.Filled.BrightnessLow,
|
||||
contentDescription = if (isOn) strings.receive.brightnessReduce // ← "Reduce brightness"
|
||||
else strings.receive.brightnessBoost, // ← "Boost brightness"
|
||||
tint = if (isOn) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ import com.bitcointxoko.gudariwallet.ui.nfc.NfcViewModel
|
||||
import com.bitcointxoko.gudariwallet.util.buildUpTaps
|
||||
import com.bitcointxoko.gudariwallet.util.fiatLabel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Composable
|
||||
fun ReceiveScreen(
|
||||
@@ -636,7 +637,7 @@ private fun ReceiveInvoiceContent(
|
||||
}
|
||||
LaunchedEffect(state.expiresAt) {
|
||||
while (secondsLeft > 0L) {
|
||||
delay(1_000L)
|
||||
delay(1_000L.milliseconds)
|
||||
secondsLeft = (state.expiresAt - System.currentTimeMillis() / 1000L)
|
||||
.coerceAtLeast(0L)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import kotlinx.coroutines.delay
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Composable
|
||||
internal fun Bolt11ConfirmCard(
|
||||
@@ -117,7 +118,7 @@ private fun ConfirmCardContent(
|
||||
var now by remember { mutableStateOf(Instant.now()) }
|
||||
LaunchedEffect(expiresAt) {
|
||||
while (true) {
|
||||
delay(1_000L)
|
||||
delay(1_000L.milliseconds)
|
||||
now = Instant.now()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.bitcointxoko.gudariwallet.ui.send
|
||||
import timber.log.Timber
|
||||
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
class PaymentPoller(private val repo: WalletRepository) {
|
||||
@@ -19,7 +20,7 @@ class PaymentPoller(private val repo: WalletRepository) {
|
||||
maxAttempts : Int = 120,
|
||||
): PollResult {
|
||||
repeat(maxAttempts) { attempt ->
|
||||
delay(intervalMs)
|
||||
delay(intervalMs.milliseconds)
|
||||
|
||||
val detail = runCatching { repo.getPaymentDetail(paymentHash) }.getOrNull()
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.bitcointxoko.gudariwallet.util.fiatLabel
|
||||
import com.bitcointxoko.gudariwallet.util.heavyClick
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Composable
|
||||
fun SendScreen(
|
||||
@@ -218,7 +219,7 @@ fun SendScreen(
|
||||
var slowPayment by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(10_000L)
|
||||
delay(10_000L.milliseconds)
|
||||
slowPayment = true
|
||||
}
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ class SendViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun emitPaymentSent(
|
||||
private fun emitPaymentSent(
|
||||
paymentHash : String,
|
||||
amountSats : Long,
|
||||
feeSats : Long?,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.bitcointxoko.gudariwallet.util
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
|
||||
// ── Epoch formatters ──────────────────────────────────────────────────────────
|
||||
//
|
||||
@@ -13,45 +17,98 @@ import java.time.format.FormatStyle
|
||||
// class-load time and become stale after a timezone/locale change).
|
||||
|
||||
/**
|
||||
* "dd MMM" — e.g. "09 Jun"
|
||||
* Suitable for compact list labels.
|
||||
* Returns "Today", "Yesterday", or null if neither applies.
|
||||
* Extracted so all three formatters share the same logic without duplication.
|
||||
*/
|
||||
fun formatEpochShort(epochSeconds: Long): String =
|
||||
DateTimeFormatter.ofPattern("dd MMM")
|
||||
.withZone(ZoneId.systemDefault())
|
||||
.format(Instant.ofEpochSecond(epochSeconds))
|
||||
enum class RelativeDay { TODAY, YESTERDAY }
|
||||
private fun relativeDay(instant: Instant, zone: ZoneId = ZoneId.systemDefault()): RelativeDay? {
|
||||
val date = instant.atZone(zone).toLocalDate()
|
||||
val today = LocalDate.now(zone)
|
||||
return when (date) {
|
||||
today -> RelativeDay.TODAY
|
||||
today.minusDays(1) -> RelativeDay.YESTERDAY
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "dd MMM yyyy, HH:mm" — e.g. "09 Jun 2025, 14:32"
|
||||
* "Today" / "Yesterday" / "dd MMM" — e.g. "Today", "Yesterday", "09 Jun"
|
||||
* Suitable for compact list labels.
|
||||
*/
|
||||
fun formatEpochShort(epochSeconds: Long, todayLabel: String, yesterdayLabel: String): String {
|
||||
val instant = Instant.ofEpochSecond(epochSeconds)
|
||||
val zone = ZoneId.systemDefault()
|
||||
return when (relativeDay(instant, zone)) {
|
||||
RelativeDay.TODAY -> todayLabel
|
||||
RelativeDay.YESTERDAY -> yesterdayLabel
|
||||
null -> DateTimeFormatter.ofPattern("dd MMM").withZone(zone).format(instant)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "Today, HH:mm" / "Yesterday, HH:mm" / "dd MMM yyyy, HH:mm" — e.g. "Today, 14:32"
|
||||
* Accepts either a numeric epoch-seconds value or an ISO offset-date-time string
|
||||
* as [rawTime] fallback (for payments not yet persisted through Room).
|
||||
*/
|
||||
fun formatTimestamp(createdAt: Long?, rawTime: String): String {
|
||||
val formatter = DateTimeFormatter
|
||||
.ofPattern("dd MMM yyyy, HH:mm")
|
||||
.withZone(ZoneId.systemDefault())
|
||||
if (createdAt != null && createdAt > 1_000_000_000L) {
|
||||
return formatter.format(Instant.ofEpochSecond(createdAt))
|
||||
fun formatTimestamp(createdAt: Long?, rawTime: String, todayLabel: String, yesterdayLabel: String): String {
|
||||
val zone = ZoneId.systemDefault()
|
||||
val timeFmt = DateTimeFormatter.ofPattern("HH:mm").withZone(zone)
|
||||
val fullFmt = DateTimeFormatter.ofPattern("dd MMM yyyy, HH:mm").withZone(zone)
|
||||
|
||||
fun format(instant: Instant): String {
|
||||
return when (relativeDay(instant, zone)) {
|
||||
RelativeDay.TODAY -> "$todayLabel, ${timeFmt.format(instant)}"
|
||||
RelativeDay.YESTERDAY -> "$yesterdayLabel, ${timeFmt.format(instant)}"
|
||||
null -> fullFmt.format(instant)
|
||||
}
|
||||
}
|
||||
|
||||
if (createdAt != null && createdAt > 1_000_000_000L) return format(Instant.ofEpochSecond(createdAt))
|
||||
return runCatching {
|
||||
formatter.format(Instant.ofEpochSecond(rawTime.toLong()))
|
||||
format(Instant.ofEpochSecond(rawTime.toLong()))
|
||||
}.recoverCatching {
|
||||
formatter.format(java.time.OffsetDateTime.parse(rawTime))
|
||||
format(java.time.OffsetDateTime.parse(rawTime).toInstant())
|
||||
}.getOrDefault(rawTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* Localized SHORT date-time — e.g. "6/9/25, 2:32 PM" (locale-dependent).
|
||||
* "Today, 2:32 PM" / "Yesterday, 2:32 PM" / localized SHORT date-time (locale-dependent).
|
||||
* Returns "—" for out-of-range epoch values; callers should handle sentinel
|
||||
* values (e.g. 0L = "never expires") before calling this.
|
||||
*/
|
||||
fun formatEpoch(epochSeconds: Long): String =
|
||||
fun formatEpoch(epochSeconds: Long, todayLabel: String, yesterdayLabel: String): String =
|
||||
try {
|
||||
DateTimeFormatter
|
||||
.ofLocalizedDateTime(FormatStyle.SHORT)
|
||||
.withZone(ZoneId.systemDefault())
|
||||
.format(Instant.ofEpochSecond(epochSeconds))
|
||||
val zone = ZoneId.systemDefault()
|
||||
val instant = Instant.ofEpochSecond(epochSeconds)
|
||||
val raw: String = when (relativeDay(instant, zone)) {
|
||||
RelativeDay.TODAY -> "$todayLabel, ${
|
||||
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
.withZone(zone).format(instant)
|
||||
}"
|
||||
RelativeDay.YESTERDAY -> "$yesterdayLabel, ${
|
||||
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
.withZone(zone).format(instant)
|
||||
}"
|
||||
null -> DateTimeFormatter
|
||||
.ofLocalizedDateTime(FormatStyle.SHORT)
|
||||
.withZone(zone)
|
||||
.format(instant)
|
||||
}
|
||||
raw.lowercase().replaceFirstChar { it.titlecase(Locale.getDefault()) }
|
||||
} catch (e: Exception) {
|
||||
Timber.tag("DateTimeUtils").w(e, "formatEpoch: could not format epochSeconds=$epochSeconds")
|
||||
"—"
|
||||
}
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ object IntentUtils {
|
||||
* Returns a raw payment URI string if the intent carries one, else null.
|
||||
*
|
||||
* Handles:
|
||||
* - ACTION_VIEW with a recognised scheme (BTCPay "Pay in wallet", browser links)
|
||||
* - ACTION_VIEW with a recognized scheme (BTCPay "Pay in wallet", browser links)
|
||||
* - ACTION_SEND with text/plain (share sheet from another app)
|
||||
*/
|
||||
fun extractPaymentUri(intent: Intent?): String? {
|
||||
|
||||
@@ -40,7 +40,7 @@ object LnurlMetadataParser {
|
||||
/**
|
||||
* Returns the display label for a LNURL payment target.
|
||||
* If [lnurl] is a Lightning Address (contains '@'), returns it as-is.
|
||||
* Otherwise returns the [domain] of the LNURL endpoint.
|
||||
* Otherwise, returns the [domain] of the LNURL endpoint.
|
||||
*/
|
||||
fun payingToLabel(lnurl: String, domain: String): String =
|
||||
if ('@' in lnurl) lnurl else domain
|
||||
Reference in New Issue
Block a user