Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 181cf56239 | |||
| 262d5f068d | |||
| 201242c797 | |||
| 8c84eca8eb | |||
| 4e22f4d3e9 | |||
| c155f43870 |
Generated
+17
@@ -2,23 +2,40 @@
|
|||||||
<dictionary name="project">
|
<dictionary name="project">
|
||||||
<words>
|
<words>
|
||||||
<w>Aead</w>
|
<w>Aead</w>
|
||||||
|
<w>Bech</w>
|
||||||
<w>Bitrefill</w>
|
<w>Bitrefill</w>
|
||||||
|
<w>Español</w>
|
||||||
<w>Nbits</w>
|
<w>Nbits</w>
|
||||||
<w>Nostr</w>
|
<w>Nostr</w>
|
||||||
<w>Tink</w>
|
<w>Tink</w>
|
||||||
<w>amboss</w>
|
<w>amboss</w>
|
||||||
|
<w>bech</w>
|
||||||
|
<w>bitcointxoko</w>
|
||||||
|
<w>btcpay</w>
|
||||||
<w>cltv</w>
|
<w>cltv</w>
|
||||||
|
<w>getnwckey</w>
|
||||||
|
<w>gudari</w>
|
||||||
|
<w>gudariwallet</w>
|
||||||
<w>hkdf</w>
|
<w>hkdf</w>
|
||||||
<w>inflight</w>
|
<w>inflight</w>
|
||||||
|
<w>keyauth</w>
|
||||||
|
<w>lnaddr</w>
|
||||||
|
<w>lnaddress</w>
|
||||||
<w>lnbc</w>
|
<w>lnbc</w>
|
||||||
<w>lnbits</w>
|
<w>lnbits</w>
|
||||||
<w>lnurlp</w>
|
<w>lnurlp</w>
|
||||||
|
<w>lnurlw</w>
|
||||||
<w>msat</w>
|
<w>msat</w>
|
||||||
<w>msats</w>
|
<w>msats</w>
|
||||||
<w>nostr</w>
|
<w>nostr</w>
|
||||||
|
<w>nwccacherepository</w>
|
||||||
|
<w>satoshi</w>
|
||||||
<w>satoshis</w>
|
<w>satoshis</w>
|
||||||
<w>sats</w>
|
<w>sats</w>
|
||||||
<w>snackbar</w>
|
<w>snackbar</w>
|
||||||
|
<w>snull</w>
|
||||||
|
<w>tink</w>
|
||||||
|
<w>walletconnect</w>
|
||||||
</words>
|
</words>
|
||||||
</dictionary>
|
</dictionary>
|
||||||
</component>
|
</component>
|
||||||
@@ -2,24 +2,18 @@ package com.bitcointxoko.gudariwallet
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.nfc.NfcAdapter
|
import android.nfc.NfcAdapter
|
||||||
import android.nfc.Tag
|
import android.nfc.Tag
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.PowerManager
|
|
||||||
import android.provider.Settings
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import kotlinx.serialization.json.jsonPrimitive
|
|||||||
* - a JSON array ["Thanks!"]
|
* - a JSON array ["Thanks!"]
|
||||||
* - null
|
* - null
|
||||||
*
|
*
|
||||||
* Mirrors the behaviour of the old Gson FlexibleStringAdapter.
|
* Mirrors the behavior of the old Gson FlexibleStringAdapter.
|
||||||
*/
|
*/
|
||||||
object FlexibleStringSerializer : KSerializer<String?> {
|
object FlexibleStringSerializer : KSerializer<String?> {
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.bitcointxoko.gudariwallet.api
|
|||||||
import kotlinx.serialization.json.Json
|
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,
|
* All serialization/deserialization — Retrofit, DataStore caches,
|
||||||
* and WebSocket message parsing — must use this instance so that
|
* and WebSocket message parsing — must use this instance so that
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.bitcointxoko.gudariwallet.api
|
package com.bitcointxoko.gudariwallet.api
|
||||||
|
|
||||||
import com.bitcointxoko.gudariwallet.security.SecretStore
|
import com.bitcointxoko.gudariwallet.security.SecretStore
|
||||||
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -13,7 +12,6 @@ import okhttp3.MediaType.Companion.toMediaType
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
class LNbitsClient(secrets: SecretStore) {
|
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() }
|
val api: LNbitsApi by lazy { buildApi() }
|
||||||
|
|
||||||
private fun buildApi(): LNbitsApi {
|
private fun buildApi(): LNbitsApi {
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ data class SuccessAction(
|
|||||||
* - a plain string "Thanks!"
|
* - a plain string "Thanks!"
|
||||||
* - null
|
* - 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.
|
* string where it expects {. This serializer handles all three cases.
|
||||||
*
|
*
|
||||||
* Applied via @Serializable(with = SuccessActionSerializer::class) on
|
* Applied via @Serializable(with = SuccessActionSerializer::class) on
|
||||||
|
|||||||
@@ -337,10 +337,8 @@ class LNbitsWalletRepository(
|
|||||||
request : NwcRegistrationRequest
|
request : NwcRegistrationRequest
|
||||||
): NwcGetResponse {
|
): NwcGetResponse {
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"NWC [REGISTER ] pubkey=${pubkey.take(16)}… " +
|
"%snull", "NWC [REGISTER ] pubkey=${pubkey.take(16)}… " +
|
||||||
"permissions=${request.permissions} " +
|
"permissions=${request.permissions} "
|
||||||
"expires_at=${request.expires_at} " +
|
|
||||||
"budgets=${request.budgets.size}"
|
|
||||||
)
|
)
|
||||||
val result = api.registerNwcKey(
|
val result = api.registerNwcKey(
|
||||||
adminKey = secrets.adminKey(),
|
adminKey = secrets.adminKey(),
|
||||||
|
|||||||
@@ -38,8 +38,7 @@ class LightningAddressStore(private val context: Context) {
|
|||||||
.map { prefs ->
|
.map { prefs ->
|
||||||
prefs[KEY_ADDRESS].also { address ->
|
prefs[KEY_ADDRESS].also { address ->
|
||||||
_currentAddress = address
|
_currentAddress = address
|
||||||
Timber.d("LNADDR [CACHE ] read from cache: " +
|
Timber.d("LNADDR [CACHE ] read from cache: %s", if (address != null) "\"$address\"" else "(empty — nothing cached yet)") }
|
||||||
if (address != null) "\"$address\"" else "(empty — nothing cached yet)") }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun isCacheStale(): Boolean {
|
suspend fun isCacheStale(): Boolean {
|
||||||
@@ -53,9 +52,8 @@ class LightningAddressStore(private val context: Context) {
|
|||||||
val fetchedAt = prefs[KEY_FETCHED_AT] ?: 0L
|
val fetchedAt = prefs[KEY_FETCHED_AT] ?: 0L
|
||||||
val age = System.currentTimeMillis() - fetchedAt
|
val age = System.currentTimeMillis() - fetchedAt
|
||||||
val isStale = address == null || age > LNADDRESS_TTL_MS
|
val isStale = address == null || age > LNADDRESS_TTL_MS
|
||||||
Timber.d("LNADDR [TTL ] cached=${address != null} " +
|
Timber.d("%s%s", "LNADDR [TTL ] cached=${address != null} " +
|
||||||
"age=${age / 1000}s ttl=${LNADDRESS_TTL_MS / 1000}s " +
|
"age=${age / 1000}s ttl=${LNADDRESS_TTL_MS / 1000}s ", if (isStale) "→ cache stale, will fetch from API"
|
||||||
if (isStale) "→ cache stale, will fetch from API"
|
|
||||||
else "→ cache fresh, skipping API call")
|
else "→ cache fresh, skipping API call")
|
||||||
isStale
|
isStale
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class LnurlCacheRepository(context: Context) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = deserialise(entity.responseJson) ?: run {
|
val response = deserialize(entity.responseJson) ?: run {
|
||||||
Timber.w("LNURL [DESER ERR ] $key — removing corrupt entry")
|
Timber.w("LNURL [DESER ERR ] $key — removing corrupt entry")
|
||||||
evict(key)
|
evict(key)
|
||||||
return null
|
return null
|
||||||
@@ -104,7 +104,7 @@ class LnurlCacheRepository(context: Context) {
|
|||||||
runCatching { dao.deleteByKey(key) }
|
runCatching { dao.deleteByKey(key) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deserialise(jsonString: String): LnurlScanResponse? =
|
private fun deserialize(jsonString: String): LnurlScanResponse? =
|
||||||
runCatching { json.decodeFromString(LnurlScanResponse.serializer(), jsonString) }
|
runCatching { json.decodeFromString(LnurlScanResponse.serializer(), jsonString) }
|
||||||
.getOrNull()
|
.getOrNull()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.bitcointxoko.gudariwallet.i18n
|
package com.bitcointxoko.gudariwallet.i18n
|
||||||
|
|
||||||
import cafe.adriel.lyricist.LyricistStrings
|
import cafe.adriel.lyricist.LyricistStrings
|
||||||
import kotlin.String
|
|
||||||
|
|
||||||
@LyricistStrings(languageTag = "en", default = true)
|
@LyricistStrings(languageTag = "en", default = true)
|
||||||
val EnHomeStrings = AppStrings(
|
val EnHomeStrings = AppStrings(
|
||||||
@@ -55,6 +54,8 @@ val EnHomeStrings = AppStrings(
|
|||||||
details = "Details",
|
details = "Details",
|
||||||
enterValidAmount = "Enter a valid amount",
|
enterValidAmount = "Enter a valid amount",
|
||||||
satsUnit = { sats -> "$sats sats" },
|
satsUnit = { sats -> "$sats sats" },
|
||||||
|
today = "today",
|
||||||
|
yesterday = "yesterday",
|
||||||
|
|
||||||
// ── SendScreen — general ──────────────────────────────────────────────────
|
// ── SendScreen — general ──────────────────────────────────────────────────
|
||||||
send = "Send",
|
send = "Send",
|
||||||
@@ -377,6 +378,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",
|
||||||
@@ -398,6 +402,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",
|
||||||
@@ -410,6 +418,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",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.bitcointxoko.gudariwallet.i18n
|
package com.bitcointxoko.gudariwallet.i18n
|
||||||
|
|
||||||
import cafe.adriel.lyricist.LyricistStrings
|
import cafe.adriel.lyricist.LyricistStrings
|
||||||
import com.bitcointxoko.gudariwallet.i18n.NwcStrings
|
|
||||||
|
|
||||||
@LyricistStrings(languageTag = "es")
|
@LyricistStrings(languageTag = "es")
|
||||||
val EsHomeStrings = AppStrings(
|
val EsHomeStrings = AppStrings(
|
||||||
@@ -55,6 +54,8 @@ val EsHomeStrings = AppStrings(
|
|||||||
details = "Detalles",
|
details = "Detalles",
|
||||||
enterValidAmount = "Introduce una cantidad válida",
|
enterValidAmount = "Introduce una cantidad válida",
|
||||||
satsUnit = { sats -> "$sats sats" },
|
satsUnit = { sats -> "$sats sats" },
|
||||||
|
today = "hoy",
|
||||||
|
yesterday = "ayer",
|
||||||
|
|
||||||
// ── SendScreen — general ──────────────────────────────────────────────────
|
// ── SendScreen — general ──────────────────────────────────────────────────
|
||||||
send = "Enviar",
|
send = "Enviar",
|
||||||
@@ -378,6 +379,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",
|
||||||
@@ -399,6 +403,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",
|
||||||
@@ -411,5 +419,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",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ val EuStrings = AppStrings(
|
|||||||
details = "Xehetasunak",
|
details = "Xehetasunak",
|
||||||
enterValidAmount = "Sartu zenbateko baliogarri bat",
|
enterValidAmount = "Sartu zenbateko baliogarri bat",
|
||||||
satsUnit = { sats -> "$sats sat" },
|
satsUnit = { sats -> "$sats sat" },
|
||||||
|
today = "gaur",
|
||||||
|
yesterday = "atzo",
|
||||||
|
|
||||||
// ── SendScreen — general ──────────────────────────────────────────────────
|
// ── SendScreen — general ──────────────────────────────────────────────────
|
||||||
send = "Bidali",
|
send = "Bidali",
|
||||||
@@ -374,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",
|
||||||
@@ -398,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",
|
||||||
@@ -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.",
|
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",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ data class AppStrings(
|
|||||||
val details : String,
|
val details : String,
|
||||||
val enterValidAmount : String,
|
val enterValidAmount : String,
|
||||||
val satsUnit : (sats: Long) -> String,
|
val satsUnit : (sats: Long) -> String,
|
||||||
|
val today : String,
|
||||||
|
val yesterday : String,
|
||||||
|
|
||||||
// ── SendScreen — general ──────────────────────────────────────────────────
|
// ── SendScreen — general ──────────────────────────────────────────────────
|
||||||
val send : String,
|
val send : String,
|
||||||
@@ -381,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,
|
||||||
@@ -406,6 +411,10 @@ 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 permLookup : String,
|
||||||
|
val permHistory : String,
|
||||||
|
val permBalance : String,
|
||||||
|
val permInfo : String,
|
||||||
val permLookupHistory : String, // "Lookup status & transaction history"
|
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,
|
||||||
@@ -418,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,
|
||||||
)
|
)
|
||||||
@@ -1,20 +1,15 @@
|
|||||||
package com.bitcointxoko.gudariwallet.security
|
package com.bitcointxoko.gudariwallet.security
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import timber.log.Timber
|
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.core.DataStoreFactory
|
import androidx.datastore.core.DataStoreFactory
|
||||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||||
import androidx.datastore.migrations.SharedPreferencesMigration
|
import androidx.datastore.migrations.SharedPreferencesMigration
|
||||||
import androidx.datastore.migrations.SharedPreferencesView
|
|
||||||
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import androidx.core.content.edit
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton DataStore for encrypted credentials.
|
* Singleton DataStore for encrypted credentials.
|
||||||
@@ -27,8 +22,6 @@ import androidx.core.content.edit
|
|||||||
* [cleanUpLegacyPrefs] once you've confirmed migration succeeded.
|
* [cleanUpLegacyPrefs] once you've confirmed migration succeeded.
|
||||||
*/
|
*/
|
||||||
object CredentialsDataStore {
|
object CredentialsDataStore {
|
||||||
private const val TAG = "CredentialsDataStore"
|
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var instance: DataStore<Credentials>? = null
|
private var instance: DataStore<Credentials>? = null
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ class CredentialsSerializer(
|
|||||||
Timber.d("Credentials decrypted successfully — isOnboarded: ${result.isOnboarded}")
|
Timber.d("Credentials decrypted successfully — isOnboarded: ${result.isOnboarded}")
|
||||||
result
|
result
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("Decryption failed — DataStore may be corrupt or key rotated", e)
|
Timber.e("Decryption failed — DataStore may be corrupt or key rotated")
|
||||||
throw CorruptionException("Cannot read/decrypt credentials DataStore", e)
|
throw CorruptionException("Cannot read/decrypt credentials DataStore")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,12 +37,10 @@ class CredentialsSerializer(
|
|||||||
val encryptingStream = streamingAead.newEncryptingStream(output, AAD)
|
val encryptingStream = streamingAead.newEncryptingStream(output, AAD)
|
||||||
t.writeTo(encryptingStream) // returns Int — discarded by Unit return type
|
t.writeTo(encryptingStream) // returns Int — discarded by Unit return type
|
||||||
encryptingStream.close()
|
encryptingStream.close()
|
||||||
Timber.d("Credentials encrypted and written — isOnboarded: ${t.isOnboarded}, " +
|
Timber.d("%snull", "Credentials encrypted and written — isOnboarded: ${t.isOnboarded}, " +
|
||||||
"hasInvoiceKey: ${t.invoiceKey.isNotBlank()}, " +
|
"hasInvoiceKey: ${t.invoiceKey.isNotBlank()}, ")
|
||||||
"hasAdminKey: ${t.adminKey.isNotBlank()}, " +
|
|
||||||
"hasBaseUrl: ${t.baseUrl.isNotBlank()}")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("Encryption/write failed", e)
|
Timber.e("Encryption/write failed")
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ object CredentialsTinkManager {
|
|||||||
Timber.i("StreamingAead primitive acquired successfully")
|
Timber.i("StreamingAead primitive acquired successfully")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class NodeAliasService(
|
|||||||
repo.put(pubkey, alias)
|
repo.put(pubkey, alias)
|
||||||
|
|
||||||
// 4. Publish to StateFlow
|
// 4. Publish to StateFlow
|
||||||
_aliases.value = _aliases.value + (pubkey to alias)
|
_aliases.value += (pubkey to alias)
|
||||||
|
|
||||||
Timber.d("ALIAS [FETCHED ] $pubkey → \"$alias\"")
|
Timber.d("ALIAS [FETCHED ] $pubkey → \"$alias\"")
|
||||||
return alias
|
return alias
|
||||||
|
|||||||
+2
-1
@@ -30,6 +30,7 @@ import okhttp3.Request
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.WebSocket
|
import okhttp3.WebSocket
|
||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
private const val TAG = "WalletNotifService"
|
private const val TAG = "WalletNotifService"
|
||||||
|
|
||||||
@@ -271,7 +272,7 @@ class WalletNotificationService : Service() {
|
|||||||
|
|
||||||
reconnectJob?.cancel()
|
reconnectJob?.cancel()
|
||||||
reconnectJob = serviceScope.launch {
|
reconnectJob = serviceScope.launch {
|
||||||
delay(backoff)
|
delay(backoff.milliseconds)
|
||||||
openWebSocket()
|
openWebSocket()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import com.bitcointxoko.gudariwallet.ui.send.SendScreen
|
|||||||
import com.bitcointxoko.gudariwallet.ui.send.SendStrings
|
import com.bitcointxoko.gudariwallet.ui.send.SendStrings
|
||||||
import com.bitcointxoko.gudariwallet.util.SendInputType
|
import com.bitcointxoko.gudariwallet.util.SendInputType
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
// ── Tab destinations ──────────────────────────────────────────────────────────
|
// ── Tab destinations ──────────────────────────────────────────────────────────
|
||||||
// Note: label and icon are now resolved at the call site via LocalAppStrings,
|
// Note: label and icon are now resolved at the call site via LocalAppStrings,
|
||||||
@@ -205,7 +206,7 @@ fun WalletScreen(
|
|||||||
|
|
||||||
LaunchedEffect(clipboardOffer) {
|
LaunchedEffect(clipboardOffer) {
|
||||||
if (clipboardOffer is ClipboardOfferState.Detected) {
|
if (clipboardOffer is ClipboardOfferState.Detected) {
|
||||||
delay(8_000)
|
delay(8_000.milliseconds)
|
||||||
vm.dismissClipboardOffer()
|
vm.dismissClipboardOffer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,7 +216,7 @@ fun WalletScreen(
|
|||||||
|
|
||||||
LaunchedEffect(nfcOffer) {
|
LaunchedEffect(nfcOffer) {
|
||||||
if (nfcOffer is NfcOfferState.Detected) {
|
if (nfcOffer is NfcOfferState.Detected) {
|
||||||
delay(8_000)
|
delay(8_000.milliseconds)
|
||||||
nfcVm.dismissNfcOffer()
|
nfcVm.dismissNfcOffer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,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) {
|
||||||
@@ -490,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()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import com.bitcointxoko.gudariwallet.data.LightningAddressStore
|
|||||||
import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
|
import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
|
||||||
import com.bitcointxoko.gudariwallet.data.NwcCacheRepository
|
import com.bitcointxoko.gudariwallet.data.NwcCacheRepository
|
||||||
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
||||||
import com.bitcointxoko.gudariwallet.data.db.AppDatabase
|
|
||||||
import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore
|
import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore
|
||||||
import com.bitcointxoko.gudariwallet.service.NodeAliasService
|
import com.bitcointxoko.gudariwallet.service.NodeAliasService
|
||||||
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
||||||
|
|||||||
@@ -75,10 +75,9 @@ class BalanceViewModel(
|
|||||||
runCatching { repo.getBalance() }
|
runCatching { repo.getBalance() }
|
||||||
.onSuccess { balance ->
|
.onSuccess { balance ->
|
||||||
val sats = balance.sats
|
val sats = balance.sats
|
||||||
val prev = (_balanceState.value as? BalanceState.Success)?.sats
|
when (val prev = (_balanceState.value as? BalanceState.Success)?.sats) {
|
||||||
when {
|
null -> Timber.d("BALANCE [NETWORK ] $sats sats — cold load complete")
|
||||||
prev == null -> Timber.d("BALANCE [NETWORK ] $sats sats — cold load complete")
|
sats -> Timber.d("BALANCE [NETWORK ] $sats sats — matches cache, no change")
|
||||||
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})")
|
else -> Timber.d("BALANCE [NETWORK ] $sats sats — was $prev sats (diff ${sats - prev})")
|
||||||
}
|
}
|
||||||
_balanceState.value = BalanceState.Success(
|
_balanceState.value = BalanceState.Success(
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package com.bitcointxoko.gudariwallet.ui.common
|
package com.bitcointxoko.gudariwallet.ui.common
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import androidx.activity.compose.LocalActivity
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages screen brightness for QR-display screens.
|
* Manages screen brightness for QR-display screens.
|
||||||
@@ -12,12 +11,12 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberBrightnessController(): BrightnessController {
|
fun rememberBrightnessController(): BrightnessController {
|
||||||
val window = (LocalContext.current as? Activity)?.window
|
val window = (LocalActivity.current)?.window
|
||||||
var isOn by remember { mutableStateOf(false) }
|
var isOn by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Sync side-effect: no coroutine needed
|
// Sync side effect: no coroutine needed
|
||||||
SideEffect {
|
SideEffect {
|
||||||
window?.attributes = window?.attributes?.apply {
|
window?.attributes = window.attributes?.apply {
|
||||||
screenBrightness = if (isOn) WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL
|
screenBrightness = if (isOn) WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL
|
||||||
else WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
else WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||||
}
|
}
|
||||||
@@ -25,7 +24,7 @@ fun rememberBrightnessController(): BrightnessController {
|
|||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
window?.attributes = window?.attributes?.apply {
|
window?.attributes = window.attributes?.apply {
|
||||||
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ internal fun FilterBottomSheet(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
currentFilter.minCreatedAt
|
currentFilter.minCreatedAt
|
||||||
?.let { formatEpochShort(it) }
|
?.let { formatEpochShort(it, strings.today, strings.yesterday) }
|
||||||
?: strings.history.filterDateFrom_label // ← "From"
|
?: strings.history.filterDateFrom_label // ← "From"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -231,7 +231,7 @@ internal fun FilterBottomSheet(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
currentFilter.maxCreatedAt
|
currentFilter.maxCreatedAt
|
||||||
?.let { formatEpochShort(it) }
|
?.let { formatEpochShort(it, strings.today, strings.yesterday) }
|
||||||
?: strings.history.filterDateTo_label // ← "To"
|
?: strings.history.filterDateTo_label // ← "To"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -464,13 +464,13 @@ private fun ActiveFilterChips(
|
|||||||
null -> when {
|
null -> when {
|
||||||
minCreatedAt != null && maxCreatedAt != null ->
|
minCreatedAt != null && maxCreatedAt != null ->
|
||||||
strings.history.filterDateRange( // ← "X–Y"
|
strings.history.filterDateRange( // ← "X–Y"
|
||||||
formatEpochShort(minCreatedAt),
|
formatEpochShort(minCreatedAt, strings.today, strings.yesterday),
|
||||||
formatEpochShort(maxCreatedAt)
|
formatEpochShort(maxCreatedAt, strings.today, strings.yesterday)
|
||||||
)
|
)
|
||||||
minCreatedAt != null ->
|
minCreatedAt != null ->
|
||||||
strings.history.filterDateFrom(formatEpochShort(minCreatedAt)) // ← "From X"
|
strings.history.filterDateFrom(formatEpochShort(minCreatedAt, strings.today, strings.yesterday)) // ← "From X"
|
||||||
else ->
|
else ->
|
||||||
strings.history.filterDateUntil(formatEpochShort(maxCreatedAt!!)) // ← "Until X"
|
strings.history.filterDateUntil(formatEpochShort(maxCreatedAt!!, strings.today, strings.yesterday)) // ← "Until X"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InputChip(
|
InputChip(
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
private const val TAG = "HistoryViewModel"
|
private const val TAG = "HistoryViewModel"
|
||||||
|
|
||||||
@@ -156,7 +157,7 @@ class HistoryViewModel(
|
|||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
combine(
|
combine(
|
||||||
_filter.map { it.searchQuery }.debounce(300),
|
_filter.map { it.searchQuery }.debounce(300.milliseconds),
|
||||||
_filter
|
_filter
|
||||||
) { _, f -> f }
|
) { _, f -> f }
|
||||||
.collect { f ->
|
.collect { f ->
|
||||||
@@ -164,10 +165,8 @@ class HistoryViewModel(
|
|||||||
Timber.d("FILTER [IN-MEMORY] filter=default → using cached state")
|
Timber.d("FILTER [IN-MEMORY] filter=default → using cached state")
|
||||||
_roomResults.value = null
|
_roomResults.value = null
|
||||||
} else {
|
} else {
|
||||||
Timber.d("FILTER [ROOM ] q=\"${f.searchQuery}\" " +
|
Timber.d("%snull", "FILTER [ROOM ] q=\"${f.searchQuery}\" " +
|
||||||
"statuses=${f.statuses} dir=${f.direction} types=${f.types} " +
|
"statuses=${f.statuses} dir=${f.direction} types=${f.types} ")
|
||||||
"amt=${f.minAmountSat}–${f.maxAmountSat} " +
|
|
||||||
"date=${f.minCreatedAt}–${f.maxCreatedAt}")
|
|
||||||
_roomResults.value = paymentCache.queryAll(f.searchQuery, f)
|
_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.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.ArrowDownward
|
import androidx.compose.material.icons.filled.ArrowDownward
|
||||||
import androidx.compose.material.icons.filled.ArrowUpward
|
import androidx.compose.material.icons.filled.ArrowUpward
|
||||||
import androidx.compose.material.icons.outlined.ContentCopy
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -234,7 +233,11 @@ private fun PaymentDetailContent(
|
|||||||
// ── Basic info ───────────────────────────────────────────────────────
|
// ── Basic info ───────────────────────────────────────────────────────
|
||||||
item {
|
item {
|
||||||
DetailSection(title = strings.history.detailSectionDetails) { // ← "Details"
|
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(
|
DetailRow(
|
||||||
strings.history.detailRowMemo, // ← "Memo"
|
strings.history.detailRowMemo, // ← "Memo"
|
||||||
payment.memo?.takeIf { it.isNotBlank() } ?: strings.history.detailRowMemoEmpty // ← "—"
|
payment.memo?.takeIf { it.isNotBlank() } ?: strings.history.detailRowMemoEmpty // ← "—"
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ internal fun PaymentRow(
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.height(2.dp))
|
Spacer(Modifier.height(2.dp))
|
||||||
val formattedTime = remember(payment.createdAt ?: payment.time) {
|
val formattedTime = remember(payment.createdAt ?: payment.time) {
|
||||||
formatTimestamp(payment.createdAt, payment.time)
|
formatTimestamp(payment.createdAt, payment.time, strings.today, strings.yesterday)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = formattedTime,
|
text = formattedTime,
|
||||||
|
|||||||
@@ -159,8 +159,7 @@ class PaymentSyncManager(
|
|||||||
val reachedEnd = page.size < WalletConstants.PAYMENTS_PAGE_SIZE
|
val reachedEnd = page.size < WalletConstants.PAYMENTS_PAGE_SIZE
|
||||||
|
|
||||||
if (reachedOverlap || reachedEnd) {
|
if (reachedOverlap || reachedEnd) {
|
||||||
Timber.d("PAYMENTS [SYNC DONE ] $totalNew new payment(s) across $pagesChecked page(s) — " +
|
Timber.d("null%s", if (reachedOverlap) "stopped at known record" else "reached end of history")
|
||||||
if (reachedOverlap) "stopped at known record" else "reached end of history")
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,13 +77,12 @@ class NdefHceService : HostApduService() {
|
|||||||
|
|
||||||
val ins = apdu[1]
|
val ins = apdu[1]
|
||||||
|
|
||||||
return when {
|
return when (ins) {
|
||||||
// ── SELECT (INS = 0xA4) ──────────────────────────────────────────
|
// ── SELECT (INS = 0xA4) ──────────────────────────────────────────
|
||||||
ins == 0xA4.toByte() -> handleSelect(apdu)
|
0xA4.toByte() -> handleSelect(apdu)
|
||||||
|
|
||||||
// ── READ BINARY (INS = 0xB0) ─────────────────────────────────────
|
// ── READ BINARY (INS = 0xB0) ─────────────────────────────────────
|
||||||
ins == 0xB0.toByte() -> handleReadBinary(apdu)
|
0xB0.toByte() -> handleReadBinary(apdu)
|
||||||
|
|
||||||
else -> SW_UNKNOWN
|
else -> SW_UNKNOWN
|
||||||
}.also { Timber.d("APDU OUT: ${it.toHex()}") }
|
}.also { Timber.d("APDU OUT: ${it.toHex()}") }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.bitcointxoko.gudariwallet.ui.NfcOfferState
|
import com.bitcointxoko.gudariwallet.ui.NfcOfferState
|
||||||
import com.bitcointxoko.gudariwallet.util.SendInputDetector
|
import com.bitcointxoko.gudariwallet.util.SendInputDetector
|
||||||
import com.bitcointxoko.gudariwallet.util.SendInputType
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -83,7 +82,7 @@ class NfcViewModel : ViewModel() {
|
|||||||
intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
|
intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rawMessages != null && rawMessages.isNotEmpty()) {
|
if (!rawMessages.isNullOrEmpty()) {
|
||||||
val message = rawMessages[0] as NdefMessage
|
val message = rawMessages[0] as NdefMessage
|
||||||
val uri = extractUri(message)
|
val uri = extractUri(message)
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
@@ -98,7 +97,7 @@ class NfcViewModel : ViewModel() {
|
|||||||
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
|
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
|
||||||
} else {
|
} else {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
|
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
|
||||||
}
|
}
|
||||||
if (tag != null) {
|
if (tag != null) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
|||||||
@@ -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.*
|
||||||
@@ -14,11 +16,15 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.bitcointxoko.gudariwallet.api.NwcGetResponse
|
import com.bitcointxoko.gudariwallet.api.NwcGetResponse
|
||||||
|
import com.bitcointxoko.gudariwallet.strings
|
||||||
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
|
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 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,
|
||||||
@@ -35,7 +41,7 @@ internal fun NwcConnectionRow(
|
|||||||
val dismissState = rememberSwipeToDismissBoxState(
|
val dismissState = rememberSwipeToDismissBoxState(
|
||||||
confirmValueChange = {
|
confirmValueChange = {
|
||||||
// Always reject — we never want the row to stay dismissed.
|
// 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
|
false
|
||||||
},
|
},
|
||||||
positionalThreshold = { totalDistance -> totalDistance * 0.4f }
|
positionalThreshold = { totalDistance -> totalDistance * 0.4f }
|
||||||
@@ -47,7 +53,7 @@ internal fun NwcConnectionRow(
|
|||||||
var dialogTriggered by remember { mutableStateOf(false) }
|
var dialogTriggered by remember { mutableStateOf(false) }
|
||||||
val scope = rememberCoroutineScope()
|
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) {
|
LaunchedEffect(dismissState.targetValue) {
|
||||||
if (dismissState.targetValue == SwipeToDismissBoxValue.EndToStart
|
if (dismissState.targetValue == SwipeToDismissBoxValue.EndToStart
|
||||||
&& !isDeleting
|
&& !isDeleting
|
||||||
@@ -84,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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -110,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,
|
||||||
@@ -120,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 ${formatEpoch(key.last_used)}",
|
else lastUsedText,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@@ -134,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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -92,8 +92,6 @@ fun ConnectionDetailScreen(
|
|||||||
onRevoke : (() -> Unit)? = null // optional – show Revoke button when provided
|
onRevoke : (() -> Unit)? = null // optional – show Revoke button when provided
|
||||||
) {
|
) {
|
||||||
val strings = LocalAppStrings.current
|
val strings = LocalAppStrings.current
|
||||||
val clipboard = LocalClipboard.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -218,13 +216,13 @@ private fun ConnectionDetailContent(
|
|||||||
|
|
||||||
DetailRow(
|
DetailRow(
|
||||||
label = strings.nwc.connectionRowCreated, // ← "Connected"
|
label = strings.nwc.connectionRowCreated, // ← "Connected"
|
||||||
value = formatEpoch(connection.createdAt)
|
value = formatEpoch(connection.createdAt, strings.today, strings.yesterday)
|
||||||
)
|
)
|
||||||
|
|
||||||
DetailRow(
|
DetailRow(
|
||||||
label = strings.nwc.connectionRowLastUsed, // ← "Last used"
|
label = strings.nwc.connectionRowLastUsed, // ← "Last used"
|
||||||
value = if (connection.lastUsed > 0)
|
value = if (connection.lastUsed > 0)
|
||||||
formatEpoch(connection.lastUsed)
|
formatEpoch(connection.lastUsed, strings.today, strings.yesterday)
|
||||||
else
|
else
|
||||||
strings.nwc.connectionNeverUsed // ← "Never"
|
strings.nwc.connectionNeverUsed // ← "Never"
|
||||||
)
|
)
|
||||||
@@ -234,9 +232,13 @@ private fun ConnectionDetailContent(
|
|||||||
value = when {
|
value = when {
|
||||||
connection.expiresAt <= 0 -> strings.nwc.connectionNoExpiry // ← "Never"
|
connection.expiresAt <= 0 -> strings.nwc.connectionNoExpiry // ← "Never"
|
||||||
isExpired -> strings.nwc.connectionExpired( // ← "Expired · 12 Jan 2025"
|
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
|
// valueColor = if (isExpired) MaterialTheme.colorScheme.error else null
|
||||||
)
|
)
|
||||||
@@ -383,7 +385,7 @@ private fun BudgetCard(budget: BudgetData) {
|
|||||||
// Created at
|
// Created at
|
||||||
DetailRow(
|
DetailRow(
|
||||||
label = strings.nwc.connectionRowCreated, // ← "Created"
|
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(
|
||||||
text = permission.label(strings), // already localised
|
text = permission.label(strings), // already localized
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)")
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ fun NotificationsPage(
|
|||||||
imageVector = Icons.Default.Notifications,
|
imageVector = Icons.Default.Notifications,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(72.dp),
|
modifier = Modifier.size(72.dp),
|
||||||
// Mirror the BatteryPage tint behaviour
|
// Mirror the BatteryPage tint behavior
|
||||||
tint = if (alreadyGranted)
|
tint = if (alreadyGranted)
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
else
|
else
|
||||||
@@ -300,7 +300,7 @@ fun NotificationsPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Page 5: Battery optimisation ──────────────────────────────────────────────
|
// ── Page 5: Battery optimization ──────────────────────────────────────────────
|
||||||
@Composable
|
@Composable
|
||||||
fun BatteryPage(strings: AppStrings) {
|
fun BatteryPage(strings: AppStrings) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|||||||
@@ -13,11 +13,9 @@ 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 androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.work.WorkInfo
|
|
||||||
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||||
import com.bitcointxoko.gudariwallet.data.HistoricalSyncStore
|
import com.bitcointxoko.gudariwallet.data.HistoricalSyncStore
|
||||||
import com.bitcointxoko.gudariwallet.security.SecretStore
|
import com.bitcointxoko.gudariwallet.security.SecretStore
|
||||||
import com.bitcointxoko.gudariwallet.sync.HistoricalSyncWorker
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
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.buildUpTaps
|
||||||
import com.bitcointxoko.gudariwallet.util.fiatLabel
|
import com.bitcointxoko.gudariwallet.util.fiatLabel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ReceiveScreen(
|
fun ReceiveScreen(
|
||||||
@@ -636,7 +637,7 @@ private fun ReceiveInvoiceContent(
|
|||||||
}
|
}
|
||||||
LaunchedEffect(state.expiresAt) {
|
LaunchedEffect(state.expiresAt) {
|
||||||
while (secondsLeft > 0L) {
|
while (secondsLeft > 0L) {
|
||||||
delay(1_000L)
|
delay(1_000L.milliseconds)
|
||||||
secondsLeft = (state.expiresAt - System.currentTimeMillis() / 1000L)
|
secondsLeft = (state.expiresAt - System.currentTimeMillis() / 1000L)
|
||||||
.coerceAtLeast(0L)
|
.coerceAtLeast(0L)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import kotlinx.coroutines.delay
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun Bolt11ConfirmCard(
|
internal fun Bolt11ConfirmCard(
|
||||||
@@ -117,7 +118,7 @@ private fun ConfirmCardContent(
|
|||||||
var now by remember { mutableStateOf(Instant.now()) }
|
var now by remember { mutableStateOf(Instant.now()) }
|
||||||
LaunchedEffect(expiresAt) {
|
LaunchedEffect(expiresAt) {
|
||||||
while (true) {
|
while (true) {
|
||||||
delay(1_000L)
|
delay(1_000L.milliseconds)
|
||||||
now = Instant.now()
|
now = Instant.now()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.bitcointxoko.gudariwallet.ui.send
|
|||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
||||||
class PaymentPoller(private val repo: WalletRepository) {
|
class PaymentPoller(private val repo: WalletRepository) {
|
||||||
@@ -19,7 +20,7 @@ class PaymentPoller(private val repo: WalletRepository) {
|
|||||||
maxAttempts : Int = 120,
|
maxAttempts : Int = 120,
|
||||||
): PollResult {
|
): PollResult {
|
||||||
repeat(maxAttempts) { attempt ->
|
repeat(maxAttempts) { attempt ->
|
||||||
delay(intervalMs)
|
delay(intervalMs.milliseconds)
|
||||||
|
|
||||||
val detail = runCatching { repo.getPaymentDetail(paymentHash) }.getOrNull()
|
val detail = runCatching { repo.getPaymentDetail(paymentHash) }.getOrNull()
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import com.bitcointxoko.gudariwallet.util.fiatLabel
|
|||||||
import com.bitcointxoko.gudariwallet.util.heavyClick
|
import com.bitcointxoko.gudariwallet.util.heavyClick
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SendScreen(
|
fun SendScreen(
|
||||||
@@ -218,7 +219,7 @@ fun SendScreen(
|
|||||||
var slowPayment by remember { mutableStateOf(false) }
|
var slowPayment by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
delay(10_000L)
|
delay(10_000L.milliseconds)
|
||||||
slowPayment = true
|
slowPayment = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ class SendViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun emitPaymentSent(
|
private fun emitPaymentSent(
|
||||||
paymentHash : String,
|
paymentHash : String,
|
||||||
amountSats : Long,
|
amountSats : Long,
|
||||||
feeSats : Long?,
|
feeSats : Long?,
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
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.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.time.format.FormatStyle
|
import java.time.format.FormatStyle
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
// ── Epoch formatters ──────────────────────────────────────────────────────────
|
// ── Epoch formatters ──────────────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
@@ -13,45 +17,98 @@ import java.time.format.FormatStyle
|
|||||||
// class-load time and become stale after a timezone/locale change).
|
// class-load time and become stale after a timezone/locale change).
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "dd MMM" — e.g. "09 Jun"
|
* Returns "Today", "Yesterday", or null if neither applies.
|
||||||
* Suitable for compact list labels.
|
* Extracted so all three formatters share the same logic without duplication.
|
||||||
*/
|
*/
|
||||||
fun formatEpochShort(epochSeconds: Long): String =
|
enum class RelativeDay { TODAY, YESTERDAY }
|
||||||
DateTimeFormatter.ofPattern("dd MMM")
|
private fun relativeDay(instant: Instant, zone: ZoneId = ZoneId.systemDefault()): RelativeDay? {
|
||||||
.withZone(ZoneId.systemDefault())
|
val date = instant.atZone(zone).toLocalDate()
|
||||||
.format(Instant.ofEpochSecond(epochSeconds))
|
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
|
* Accepts either a numeric epoch-seconds value or an ISO offset-date-time string
|
||||||
* as [rawTime] fallback (for payments not yet persisted through Room).
|
* as [rawTime] fallback (for payments not yet persisted through Room).
|
||||||
*/
|
*/
|
||||||
fun formatTimestamp(createdAt: Long?, rawTime: String): String {
|
fun formatTimestamp(createdAt: Long?, rawTime: String, todayLabel: String, yesterdayLabel: String): String {
|
||||||
val formatter = DateTimeFormatter
|
val zone = ZoneId.systemDefault()
|
||||||
.ofPattern("dd MMM yyyy, HH:mm")
|
val timeFmt = DateTimeFormatter.ofPattern("HH:mm").withZone(zone)
|
||||||
.withZone(ZoneId.systemDefault())
|
val fullFmt = DateTimeFormatter.ofPattern("dd MMM yyyy, HH:mm").withZone(zone)
|
||||||
if (createdAt != null && createdAt > 1_000_000_000L) {
|
|
||||||
return formatter.format(Instant.ofEpochSecond(createdAt))
|
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 {
|
return runCatching {
|
||||||
formatter.format(Instant.ofEpochSecond(rawTime.toLong()))
|
format(Instant.ofEpochSecond(rawTime.toLong()))
|
||||||
}.recoverCatching {
|
}.recoverCatching {
|
||||||
formatter.format(java.time.OffsetDateTime.parse(rawTime))
|
format(java.time.OffsetDateTime.parse(rawTime).toInstant())
|
||||||
}.getOrDefault(rawTime)
|
}.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
|
* Returns "—" for out-of-range epoch values; callers should handle sentinel
|
||||||
* values (e.g. 0L = "never expires") before calling this.
|
* values (e.g. 0L = "never expires") before calling this.
|
||||||
*/
|
*/
|
||||||
fun formatEpoch(epochSeconds: Long): String =
|
fun formatEpoch(epochSeconds: Long, todayLabel: String, yesterdayLabel: String): String =
|
||||||
try {
|
try {
|
||||||
DateTimeFormatter
|
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)
|
.ofLocalizedDateTime(FormatStyle.SHORT)
|
||||||
.withZone(ZoneId.systemDefault())
|
.withZone(zone)
|
||||||
.format(Instant.ofEpochSecond(epochSeconds))
|
.format(instant)
|
||||||
|
}
|
||||||
|
raw.lowercase().replaceFirstChar { it.titlecase(Locale.getDefault()) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("DateTimeUtils").w(e, "formatEpoch: could not format epochSeconds=$epochSeconds")
|
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.
|
* Returns a raw payment URI string if the intent carries one, else null.
|
||||||
*
|
*
|
||||||
* Handles:
|
* 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)
|
* - ACTION_SEND with text/plain (share sheet from another app)
|
||||||
*/
|
*/
|
||||||
fun extractPaymentUri(intent: Intent?): String? {
|
fun extractPaymentUri(intent: Intent?): String? {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ object LnurlMetadataParser {
|
|||||||
/**
|
/**
|
||||||
* Returns the display label for a LNURL payment target.
|
* Returns the display label for a LNURL payment target.
|
||||||
* If [lnurl] is a Lightning Address (contains '@'), returns it as-is.
|
* 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 =
|
fun payingToLabel(lnurl: String, domain: String): String =
|
||||||
if ('@' in lnurl) lnurl else domain
|
if ('@' in lnurl) lnurl else domain
|
||||||
Reference in New Issue
Block a user