diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/api/Models.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/api/Models.kt index 0078d21..7746cd5 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/api/Models.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/api/Models.kt @@ -166,7 +166,9 @@ data class PaymentRecord( @SerializedName("bolt11") val bolt11: String?, @SerializedName("pending") val pending: Boolean, @SerializedName("preimage") val preimage: String?, - @SerializedName("extra") val extra: PaymentExtra? + @SerializedName("extra") val extra: PaymentExtra?, + // Not from the API — populated only when loaded from Room via toDomain() + val createdAt: Long? = null ) { val amountSat: Long get() = kotlin.math.abs(amountMsat) / 1000L val feeSat: Long get() = kotlin.math.abs(feeMsat) / 1000L diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/AppDatabase.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/AppDatabase.kt index 74b01ca..787a5fc 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/AppDatabase.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/AppDatabase.kt @@ -12,9 +12,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase entities = [ PaymentRecordEntity::class, LnurlCacheEntity::class, - NodeAliasCacheEntity::class, // ← added + NodeAliasCacheEntity::class, ], - version = 3, // ← bumped + version = 4, // ← bumped exportSchema = false ) @TypeConverters(PaymentConverters::class) @@ -22,7 +22,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun paymentDao(): PaymentDao abstract fun lnurlCacheDao(): LnurlCacheDao - abstract fun nodeAliasCacheDao(): NodeAliasCacheDao // ← added + abstract fun nodeAliasCacheDao(): NodeAliasCacheDao companion object { @Volatile private var INSTANCE: AppDatabase? = null @@ -42,7 +42,7 @@ abstract class AppDatabase : RoomDatabase() { } } - private val MIGRATION_2_3 = object : Migration(2, 3) { // ← added + private val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL( """ @@ -57,6 +57,19 @@ abstract class AppDatabase : RoomDatabase() { } } + private val MIGRATION_3_4 = object : Migration(3, 4) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE payment_records ADD COLUMN createdAt INTEGER") + db.execSQL( + """ + UPDATE payment_records + SET createdAt = CAST(time AS INTEGER) + WHERE CAST(time AS INTEGER) > 1000000000 + """.trimIndent() + ) + } + } + fun getInstance(context: Context): AppDatabase = INSTANCE ?: synchronized(this) { INSTANCE ?: Room.databaseBuilder( @@ -64,7 +77,7 @@ abstract class AppDatabase : RoomDatabase() { AppDatabase::class.java, "gudari_wallet.db" ) - .addMigrations(MIGRATION_1_2, MIGRATION_2_3) // ← added + .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) // ← added .build() .also { INSTANCE = it } } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentRecordEntity.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentRecordEntity.kt index 4932b7e..66ae689 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentRecordEntity.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentRecordEntity.kt @@ -14,6 +14,7 @@ data class PaymentRecordEntity( val feeMsat: Long, val memo: String?, val time: String, + val createdAt: Long? = null, val status: String, val bolt11: String?, val pending: Boolean, diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentRecordMapper.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentRecordMapper.kt index 0706e3f..284e41d 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentRecordMapper.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentRecordMapper.kt @@ -1,8 +1,27 @@ package com.bitcointxoko.gudariwallet.data.db +import android.util.Log import com.bitcointxoko.gudariwallet.api.PaymentRecord import com.bitcointxoko.gudariwallet.data.db.PaymentRecordEntity +private const val TAG = "PaymentMappers" + + +// ── Parse helper — runs once at write time, never again ────────────────────── + +private fun parseCreatedAt(time: String): Long? { + val result = time.toLongOrNull() + ?: runCatching { + java.time.OffsetDateTime.parse(time).toEpochSecond() + }.getOrNull() + if (result == null) { + Log.w(TAG, "parseCreatedAt: could not parse time string \"$time\" — createdAt will be null") + } + return result +} + +// ── PaymentRecord → PaymentRecordEntity ────────────────────────────────────── + fun PaymentRecord.toEntity(savedAt: Long): PaymentRecordEntity = PaymentRecordEntity( checkingId = checkingId, @@ -11,6 +30,7 @@ fun PaymentRecord.toEntity(savedAt: Long): PaymentRecordEntity = feeMsat = feeMsat, memo = memo, time = time, + createdAt = parseCreatedAt(time), // ← NEW status = status, bolt11 = bolt11, pending = pending, @@ -19,6 +39,8 @@ fun PaymentRecord.toEntity(savedAt: Long): PaymentRecordEntity = savedAt = savedAt ) +// ── PaymentRecordEntity → PaymentRecord ────────────────────────────────────── + fun PaymentRecordEntity.toDomain(): PaymentRecord = PaymentRecord( checkingId = checkingId, @@ -27,6 +49,7 @@ fun PaymentRecordEntity.toDomain(): PaymentRecord = feeMsat = feeMsat, memo = memo, time = time, + createdAt = createdAt, // ← NEW status = status, bolt11 = bolt11, pending = pending, diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt index 4198e4c..4901c30 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt @@ -2,6 +2,7 @@ package com.bitcointxoko.gudariwallet.ui import android.content.ClipData import android.content.Intent +import android.util.Log import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -493,8 +494,11 @@ private fun PaymentRow( overflow = TextOverflow.Ellipsis ) Spacer(Modifier.height(2.dp)) + val formattedTime = remember(payment.createdAt ?: payment.time) { + formatTimestamp(payment.createdAt, payment.time) + } Text( - text = formatTimestamp(payment.time), + text = formattedTime, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -702,7 +706,7 @@ private fun PaymentDetailContent( // ── Basic info ─────────────────────────────────────────────────────── item { DetailSection(title = "Details") { - DetailRow("Date", formatTimestamp(payment.time)) + DetailRow("Date", formatTimestamp(payment.createdAt, payment.time)) DetailRow("Memo", payment.memo?.takeIf { it.isNotBlank() } ?: "—") if (payment.extra?.tag != null) { DetailRow("Type", payment.extra.tag) @@ -897,22 +901,23 @@ private fun StatusChip(status: String) { // ── Helpers ────────────────────────────────────────────────────────────────── -private fun formatTimestamp(time: String): String { +private fun formatTimestamp(createdAt: Long?, rawTime: String): String { + Log.d("TimestampDebug", "createdAt=$createdAt rawTime=$rawTime") + 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 { - val epochSeconds = time.toLong() - java.time.format.DateTimeFormatter - .ofPattern("dd MMM yyyy, HH:mm") - .withZone(java.time.ZoneId.systemDefault()) - .format(java.time.Instant.ofEpochSecond(epochSeconds)) + formatter.format(java.time.Instant.ofEpochSecond(rawTime.toLong())) }.recoverCatching { - val odt = java.time.OffsetDateTime.parse(time) - java.time.format.DateTimeFormatter - .ofPattern("dd MMM yyyy, HH:mm") - .withZone(java.time.ZoneId.systemDefault()) - .format(odt) - }.getOrDefault(time) + formatter.format(java.time.OffsetDateTime.parse(rawTime)) + }.getOrDefault(rawTime) } + @Composable private fun AmountWithFiatColumn( amountSat : Long, diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryViewModel.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryViewModel.kt index 145f5b8..31e5f09 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryViewModel.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryViewModel.kt @@ -194,7 +194,8 @@ class HistoryViewModel( if (toMerge.isNotEmpty()) { paymentCache.mergePayments(toMerge) totalNew += toMerge.size - Log.d(TAG, "PAYMENTS [SYNC PAGE ] page $pagesChecked — ${toMerge.size} new payments merged") + val withTimestamp = toMerge.count { it.time.toLongOrNull() != null } + Log.d(TAG, "PAYMENTS [SYNC PAGE ] page $pagesChecked — ${toMerge.size} new payments merged ($withTimestamp with numeric timestamp)") } val reachedOverlap = overlapIndex != -1