refactor: DateTimeUtils and PaymentUtils

This commit is contained in:
2026-06-10 18:48:16 +02:00
parent 2bc92fca06
commit e71a0ffdb9
10 changed files with 74 additions and 48 deletions
@@ -32,6 +32,7 @@ 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.LocalAppStrings import com.bitcointxoko.gudariwallet.LocalAppStrings
import com.bitcointxoko.gudariwallet.util.formatEpochShort
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -59,6 +59,7 @@ import androidx.compose.ui.unit.dp
import com.bitcointxoko.gudariwallet.api.PaymentRecord import com.bitcointxoko.gudariwallet.api.PaymentRecord
import com.bitcointxoko.gudariwallet.LocalAppStrings import com.bitcointxoko.gudariwallet.LocalAppStrings
import com.bitcointxoko.gudariwallet.ui.HistoryState import com.bitcointxoko.gudariwallet.ui.HistoryState
import com.bitcointxoko.gudariwallet.util.formatEpochShort
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
// ── List screen ────────────────────────────────────────────────────────────── // ── List screen ──────────────────────────────────────────────────────────────
@@ -4,36 +4,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
import com.bitcointxoko.gudariwallet.util.isFailedStatus
// ── Pure helpers ───────────────────────────────────────────────────────────── import com.bitcointxoko.gudariwallet.util.isPendingStatus
internal fun formatEpochShort(epoch: Long): String =
java.time.format.DateTimeFormatter.ofPattern("dd MMM")
.withZone(java.time.ZoneId.systemDefault())
.format(java.time.Instant.ofEpochSecond(epoch))
internal fun formatTimestamp(createdAt: Long?, rawTime: String): String {
val formatter = java.time.format.DateTimeFormatter
.ofPattern("dd MMM yyyy, HH:mm")
.withZone(java.time.ZoneId.systemDefault())
if (createdAt != null && createdAt > 1_000_000_000L) { // sanity check
return formatter.format(java.time.Instant.ofEpochSecond(createdAt))
}
// Fallback: only reached for payments not yet written through Room
return runCatching {
formatter.format(java.time.Instant.ofEpochSecond(rawTime.toLong()))
}.recoverCatching {
formatter.format(java.time.OffsetDateTime.parse(rawTime))
}.getOrDefault(rawTime)
}
internal fun isPendingStatus(status: String): Boolean =
status.lowercase() in setOf("pending", "in_flight", "inflight")
internal fun isFailedStatus(status: String): Boolean =
status.lowercase() in setOf("failed", "error", "expired")
// ── Composable helpers ───────────────────────────────────────────────────────
@Composable @Composable
internal fun paymentAmountColor(status: String, isOutgoing: Boolean): Color { internal fun paymentAmountColor(status: String, isOutgoing: Boolean): Color {
@@ -47,6 +47,7 @@ import com.bitcointxoko.gudariwallet.LocalAppStrings
import com.bitcointxoko.gudariwallet.ui.DetailState import com.bitcointxoko.gudariwallet.ui.DetailState
import com.bitcointxoko.gudariwallet.util.feePpm import com.bitcointxoko.gudariwallet.util.feePpm
import com.bitcointxoko.gudariwallet.util.formatFiatForSats import com.bitcointxoko.gudariwallet.util.formatFiatForSats
import com.bitcointxoko.gudariwallet.util.formatTimestamp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
// ── Detail screen ──────────────────────────────────────────────────────────── // ── Detail screen ────────────────────────────────────────────────────────────
@@ -26,6 +26,9 @@ import com.bitcointxoko.gudariwallet.api.PaymentRecord
import com.bitcointxoko.gudariwallet.LocalAppStrings import com.bitcointxoko.gudariwallet.LocalAppStrings
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
import com.bitcointxoko.gudariwallet.util.formatFiatForSats import com.bitcointxoko.gudariwallet.util.formatFiatForSats
import com.bitcointxoko.gudariwallet.util.formatTimestamp
import com.bitcointxoko.gudariwallet.util.isFailedStatus
import com.bitcointxoko.gudariwallet.util.isPendingStatus
@Composable @Composable
internal fun PaymentRow( internal fun PaymentRow(
@@ -15,6 +15,7 @@ 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.ui.theme.semanticColors import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
import com.bitcointxoko.gudariwallet.util.formatEpoch
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Instant import java.time.Instant
@@ -62,6 +62,7 @@ import com.bitcointxoko.gudariwallet.LocalAppStrings
import com.bitcointxoko.gudariwallet.i18n.AppStrings import com.bitcointxoko.gudariwallet.i18n.AppStrings
import com.bitcointxoko.gudariwallet.ui.history.DetailRow import com.bitcointxoko.gudariwallet.ui.history.DetailRow
import com.bitcointxoko.gudariwallet.ui.history.DetailSection import com.bitcointxoko.gudariwallet.ui.history.DetailSection
import com.bitcointxoko.gudariwallet.util.formatEpoch
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
// ── ConnectionDetailScreen.kt ───────────────────────────────────────────────── // ── ConnectionDetailScreen.kt ─────────────────────────────────────────────────
@@ -1,18 +0,0 @@
package com.bitcointxoko.gudariwallet.ui.nwc
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
// ── Date / time formatting ────────────────────────────────────────────
internal val dateFormatter: DateTimeFormatter =
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
.withZone(ZoneId.systemDefault())
/** Formats an epoch-seconds timestamp into a localized human-readable string.
* Returns "—" if the timestamp cannot be formatted. */
internal fun formatEpoch(epochSeconds: Long): String =
runCatching { dateFormatter.format(Instant.ofEpochSecond(epochSeconds)) }
.getOrDefault("")
@@ -0,0 +1,57 @@
package com.bitcointxoko.gudariwallet.util
import timber.log.Timber
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
// ── Epoch formatters ──────────────────────────────────────────────────────────
//
// All formatters are built on each call so that ZoneId.systemDefault() and the
// current locale are always fresh (a file-level val would capture them once at
// class-load time and become stale after a timezone/locale change).
/**
* "dd MMM" — e.g. "09 Jun"
* Suitable for compact list labels.
*/
fun formatEpochShort(epochSeconds: Long): String =
DateTimeFormatter.ofPattern("dd MMM")
.withZone(ZoneId.systemDefault())
.format(Instant.ofEpochSecond(epochSeconds))
/**
* "dd MMM yyyy, HH:mm" — e.g. "09 Jun 2025, 14:32"
* Accepts either a numeric epoch-seconds value or an ISO offset-date-time string
* as [rawTime] fallback (for payments not yet persisted through Room).
*/
fun formatTimestamp(createdAt: Long?, rawTime: String): String {
val formatter = DateTimeFormatter
.ofPattern("dd MMM yyyy, HH:mm")
.withZone(ZoneId.systemDefault())
if (createdAt != null && createdAt > 1_000_000_000L) {
return formatter.format(Instant.ofEpochSecond(createdAt))
}
return runCatching {
formatter.format(Instant.ofEpochSecond(rawTime.toLong()))
}.recoverCatching {
formatter.format(java.time.OffsetDateTime.parse(rawTime))
}.getOrDefault(rawTime)
}
/**
* Localized SHORT date-time — e.g. "6/9/25, 2:32 PM" (locale-dependent).
* Returns "—" for out-of-range epoch values; callers should handle sentinel
* values (e.g. 0L = "never expires") before calling this.
*/
fun formatEpoch(epochSeconds: Long): String =
try {
DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.SHORT)
.withZone(ZoneId.systemDefault())
.format(Instant.ofEpochSecond(epochSeconds))
} catch (e: Exception) {
Timber.tag("DateTimeUtils").w(e, "formatEpoch: could not format epochSeconds=$epochSeconds")
""
}
@@ -0,0 +1,7 @@
package com.bitcointxoko.gudariwallet.util
fun isPendingStatus(status: String): Boolean =
status.lowercase() in setOf("pending", "in_flight", "inflight")
fun isFailedStatus(status: String): Boolean =
status.lowercase() in setOf("failed", "error", "expired")