Skip to content
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
4327451
fix: gate sheets and rotate address on restore
ovitrif Nov 7, 2025
ba1e164
fix: gate new transaction sheet on restore
ovitrif Nov 7, 2025
e146b1c
refactor: unify should backup check
ovitrif Nov 7, 2025
9823b7f
chore: Cleanup and add todo
ovitrif Nov 7, 2025
bc53574
feat: dismiss keyboard on valid words paste
ovitrif Nov 7, 2025
918f111
refactor: encapsulate wallet init logic
ovitrif Nov 7, 2025
4aaa73b
chore: update bitkit-core to 0.1.24
ovitrif Nov 7, 2025
3fb3b06
refactor: use core tag metadata model
ovitrif Nov 7, 2025
ab5bacf
feat: backup & restore activity tags
ovitrif Nov 7, 2025
0ea7ab9
refactor: migrate RestoreWalletScreen to MVVM
ovitrif Nov 7, 2025
970a6b3
feat: nav to previous input on backspace if empty
ovitrif Nov 8, 2025
ce10188
feat: bold text in focused input
ovitrif Nov 8, 2025
7e22f02
fix: avoid validation errors on focused input
ovitrif Nov 8, 2025
ac21d53
feat: use bitkit-core for bip39 & checksum
ovitrif Nov 8, 2025
48534ef
feat: wipe core db on wipe wallet
ovitrif Nov 8, 2025
6a40ee1
feat: reset logs on wipe wallet
ovitrif Nov 11, 2025
896109a
Merge branch 'fix/rotate-address' into feat/backup-polish
ovitrif Nov 11, 2025
1ac1eb9
feat: reset blocktank repo data on wipe
ovitrif Nov 11, 2025
4e48d95
fix: logger crash in unit tests
ovitrif Nov 12, 2025
bfc497c
refactor: add activity.txType extension
ovitrif Nov 13, 2025
ca35a69
feat: reset activity state and wipe fixes
ovitrif Nov 14, 2025
4ecd67b
feat: integrate bitkit-core 0.1.27 minimally
ovitrif Nov 14, 2025
3efa06f
Merge branch 'master' into feat/backup-polish
ovitrif Nov 14, 2025
6effdff
chore: lint
ovitrif Nov 14, 2025
42a48c7
feat: use payload models for settings and widgets
ovitrif Nov 17, 2025
e699c54
fix: preserve backup times & fix race condition
ovitrif Nov 17, 2025
df10340
chore: backup status docs & comments
ovitrif Nov 17, 2025
67cfcf9
chore: fix params compiler ambiguity
ovitrif Nov 17, 2025
3aa6ff8
fix: notify observers after activity restore
ovitrif Nov 17, 2025
b5d724c
fix: restore wallet input cursor & text style
ovitrif Nov 17, 2025
b634464
refactor: extract wipe wallet use case
ovitrif Nov 17, 2025
c359bad
test: wipe wallet use case
ovitrif Nov 17, 2025
d831803
refactor: split restore screen content
ovitrif Nov 17, 2025
1f97313
chore: lint
ovitrif Nov 17, 2025
9994130
refactor: extract bip39 service
ovitrif Nov 17, 2025
d1f8e8e
test: restore screen viewmodel
ovitrif Nov 17, 2025
3ebd9d5
feat: backup relative dates
ovitrif Nov 18, 2025
59d9b48
chore: lint
ovitrif Nov 18, 2025
daed5db
fix: backup relative dates
ovitrif Nov 18, 2025
342e144
test: fix syncActivities success flow test
ovitrif Nov 18, 2025
1a47b85
test: validate wipe order
ovitrif Nov 18, 2025
907a54d
fix: support tab and newline mnemonic separators
ovitrif Nov 18, 2025
e173ae6
fix: dependencies repositories ordering
ovitrif Nov 18, 2025
f0e8371
chore: enable dynamic agent loading explicitly
ovitrif Nov 18, 2025
c6ed583
fix: clear widgets data on wipe
ovitrif Nov 19, 2025
0573515
Merge branch 'master' into feat/backup-polish
ovitrif Nov 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ val keystoreProperties by lazy {
keystoreProperties
}

val locales = listOf("en", "ar", "ca", "cs", "de", "el", "es", "fr", "it", "nl", "pl", "pt", "ru")

android {
namespace = "to.bitkit"
compileSdk = 35
Expand All @@ -49,6 +51,7 @@ android {
}
buildConfigField("boolean", "E2E", System.getenv("E2E")?.toBoolean()?.toString() ?: "false")
buildConfigField("boolean", "GEO", System.getenv("GEO")?.toBoolean()?.toString() ?: "true")
buildConfigField("String", "LOCALES", "\"${locales.joinToString(",")}\"")
}

flavorDimensions += "network"
Expand Down Expand Up @@ -131,7 +134,7 @@ android {
}
androidResources {
@Suppress("UnstableApiUsage")
localeFilters.addAll(listOf("en", "ar", "ca", "cs", "de", "el", "es", "fr", "it", "nl", "pl", "pt", "ru"))
localeFilters.addAll(locales)
@Suppress("UnstableApiUsage")
generateLocaleConfig = true
}
Expand Down
47 changes: 0 additions & 47 deletions app/detekt-baseline.xml

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions app/src/main/java/to/bitkit/data/SettingsStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import to.bitkit.env.Env
import to.bitkit.models.BitcoinDisplayUnit
import to.bitkit.models.CoinSelectionPreference
import to.bitkit.models.PrimaryDisplay
import to.bitkit.models.SettingsBackupV1
import to.bitkit.models.Suggestion
import to.bitkit.models.TransactionSpeed
import to.bitkit.utils.Logger
Expand All @@ -31,6 +32,14 @@ class SettingsStore @Inject constructor(

val data: Flow<SettingsData> = store.data

suspend fun restoreFromBackup(payload: SettingsBackupV1) =
runCatching {
val data = payload.settings.resetPin()
store.updateData { data }
}.onSuccess {
Logger.debug("Restored settings", TAG)
}

suspend fun update(transform: (SettingsData) -> SettingsData) {
store.updateData(transform)
}
Expand Down Expand Up @@ -62,6 +71,7 @@ class SettingsStore @Inject constructor(
}

companion object {
private const val TAG = "SettingsStore"
private const val MAX_LAST_USED_TAGS = 10
}
}
Expand Down
23 changes: 16 additions & 7 deletions app/src/main/java/to/bitkit/data/WidgetsStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import to.bitkit.data.dto.price.PriceDTO
import to.bitkit.data.serializers.WidgetsSerializer
import to.bitkit.models.WidgetType
import to.bitkit.models.WidgetWithPosition
import to.bitkit.models.WidgetsBackupV1
import to.bitkit.models.widget.BlocksPreferences
import to.bitkit.models.widget.CalculatorValues
import to.bitkit.models.widget.FactsPreferences
Expand Down Expand Up @@ -43,9 +44,13 @@ class WidgetsStore @Inject constructor(
val weatherFlow: Flow<WeatherDTO?> = data.map { it.weather }
val priceFlow: Flow<PriceDTO?> = data.map { it.price }

suspend fun update(transform: (WidgetsData) -> WidgetsData) {
store.updateData(transform)
}
suspend fun restoreFromBackup(payload: WidgetsBackupV1) =
runCatching {
val data = payload.widgets
store.updateData { data }
}.onSuccess {
Logger.debug("Restored widgets", TAG)
}

suspend fun updateCalculatorValues(calculatorValues: CalculatorValues) {
store.updateData {
Expand Down Expand Up @@ -127,16 +132,16 @@ class WidgetsStore @Inject constructor(
suspend fun addWidget(type: WidgetType) {
if (store.data.first().widgets.map { it.type }.contains(type)) return

store.updateData {
it.copy(widgets = (it.widgets + WidgetWithPosition(type = type)).sortedBy { it.position })
store.updateData { data ->
data.copy(widgets = (data.widgets + WidgetWithPosition(type = type)).sortedBy { it.position })
}
}

suspend fun deleteWidget(type: WidgetType) {
if (!store.data.first().widgets.map { it.type }.contains(type)) return

store.updateData {
it.copy(widgets = it.widgets.filterNot { it.type == type })
store.updateData { data ->
data.copy(widgets = data.widgets.filterNot { it.type == type })
}
}

Expand All @@ -145,6 +150,10 @@ class WidgetsStore @Inject constructor(
it.copy(widgets = widgets)
}
}

companion object {
private const val TAG = "WidgetsStore"
}
}

@Serializable
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/to/bitkit/env/Env.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal object Env {
const val isE2eTest = BuildConfig.E2E
const val isGeoblockingEnabled = BuildConfig.GEO
val network = Network.valueOf(BuildConfig.NETWORK)
val locales = BuildConfig.LOCALES.split(",")
val walletSyncIntervalSecs = 10_uL // TODO review
val platform = "Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})"
const val version = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
Expand Down Expand Up @@ -116,10 +117,10 @@ internal object Env {
Logger.info("App storage path: $path")
}

val logDir: String
val logDir: File
get() {
require(::appStoragePath.isInitialized)
return File(appStoragePath).resolve("logs").ensureDir().path
return File(appStoragePath).resolve("logs").ensureDir()
}

fun ldkStoragePath(walletIndex: Int) = storagePathOf(walletIndex, network.name.lowercase(), "ldk")
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/to/bitkit/ext/Activities.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ fun Activity.rawId(): String = when (this) {
is Activity.Onchain -> v1.id
}

fun Activity.txType(): PaymentType = when (this) {
is Activity.Lightning -> v1.txType
is Activity.Onchain -> v1.txType
}

/**
* Calculates the total value of an activity based on its type.
*
Expand Down
133 changes: 96 additions & 37 deletions app/src/main/java/to/bitkit/ext/DateTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
package to.bitkit.ext

import android.icu.text.DateFormat
import android.icu.text.DisplayContext
import android.icu.text.NumberFormat
import android.icu.text.RelativeDateTimeFormatter
import android.icu.text.RelativeDateTimeFormatter.AbsoluteUnit
import android.icu.text.RelativeDateTimeFormatter.Direction
import android.icu.text.RelativeDateTimeFormatter.RelativeUnit
import android.icu.util.ULocale
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate
Expand Down Expand Up @@ -48,11 +54,50 @@ fun Long.toDateUTC(): String {
fun Long.toLocalizedTimestamp(): String {
val uLocale = ULocale.forLocale(Locale.US)
val formatter = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT, uLocale)
?: return SimpleDateFormat("MMMM d, yyyy 'at' h:mm a", Locale.US).format(Date(this))
return formatter.format(Date(this))
}

@Suppress("LongMethod")
fun Long.toRelativeTimeString(
locale: Locale = Locale.getDefault(),
clock: Clock = Clock.System,
): String {
val now = nowMillis(clock)
val diffMillis = now - this

val uLocale = ULocale.forLocale(locale)
val numberFormat = NumberFormat.getNumberInstance(uLocale)?.apply { maximumFractionDigits = 0 }

val formatter = RelativeDateTimeFormatter.getInstance(
uLocale,
numberFormat,
RelativeDateTimeFormatter.Style.LONG,
DisplayContext.CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE,
) ?: return toLocalizedTimestamp()

val seconds = diffMillis / Factor.MILLIS_TO_SECONDS
val minutes = seconds / Factor.SECONDS_TO_MINUTES
val hours = minutes / Factor.MINUTES_TO_HOURS
val days = hours / Factor.HOURS_TO_DAYS
val weeks = days / Factor.DAYS_TO_WEEKS
val months = days / Factor.DAYS_TO_MONTHS
val years = days / Factor.DAYS_TO_YEARS

return when {
seconds < Threshold.SECONDS -> formatter.format(Direction.PLAIN, AbsoluteUnit.NOW)
minutes < Threshold.MINUTES -> formatter.format(minutes, Direction.LAST, RelativeUnit.MINUTES)
hours < Threshold.HOURS -> formatter.format(hours, Direction.LAST, RelativeUnit.HOURS)
days < Threshold.YESTERDAY -> formatter.format(Direction.LAST, AbsoluteUnit.DAY)
days < Threshold.DAYS -> formatter.format(days, Direction.LAST, RelativeUnit.DAYS)
weeks < Threshold.WEEKS -> formatter.format(weeks, Direction.LAST, RelativeUnit.WEEKS)
months < Threshold.MONTHS -> formatter.format(months, Direction.LAST, RelativeUnit.MONTHS)
else -> formatter.format(years, Direction.LAST, RelativeUnit.YEARS)
}
}

fun getDaysInMonth(month: LocalDate): List<LocalDate?> {
val firstDayOfMonth = LocalDate(month.year, month.month, CalendarConstants.FIRST_DAY_OF_MONTH)
val firstDayOfMonth = LocalDate(month.year, month.month, Constants.FIRST_DAY_OF_MONTH)
val daysInMonth = month.month.length(isLeapYear(month.year))

// Get the day of week for the first day (1 = Monday, 7 = Sunday)
Expand All @@ -70,7 +115,7 @@ fun getDaysInMonth(month: LocalDate): List<LocalDate?> {
}

// Add all days of the month
for (day in CalendarConstants.FIRST_DAY_OF_MONTH..daysInMonth) {
for (day in Constants.FIRST_DAY_OF_MONTH..daysInMonth) {
days.add(LocalDate(month.year, month.month, day))
}

Expand All @@ -83,49 +128,43 @@ fun getDaysInMonth(month: LocalDate): List<LocalDate?> {
}

fun isLeapYear(year: Int): Boolean {
return (year % CalendarConstants.LEAP_YEAR_DIVISOR_4 == 0 && year % CalendarConstants.LEAP_YEAR_DIVISOR_100 != 0) ||
(year % CalendarConstants.LEAP_YEAR_DIVISOR_400 == 0)
return (year % Constants.LEAP_YEAR_DIVISOR_4 == 0 && year % Constants.LEAP_YEAR_DIVISOR_100 != 0) ||
(year % Constants.LEAP_YEAR_DIVISOR_400 == 0)
}

fun isDateInRange(dateMillis: Long, startMillis: Long?, endMillis: Long?): Boolean {
fun isDateInRange(
dateMillis: Long,
startMillis: Long?,
endMillis: Long?,
zone: TimeZone = TimeZone.currentSystemDefault(),
): Boolean {
if (startMillis == null) return false
val end = endMillis ?: startMillis

val normalizedDate = kotlinx.datetime.Instant.fromEpochMilliseconds(dateMillis)
.toLocalDateTime(TimeZone.currentSystemDefault()).date
val normalizedStart = kotlinx.datetime.Instant.fromEpochMilliseconds(startMillis)
.toLocalDateTime(TimeZone.currentSystemDefault()).date
val normalizedEnd = kotlinx.datetime.Instant.fromEpochMilliseconds(end)
.toLocalDateTime(TimeZone.currentSystemDefault()).date
val normalizedDate = kotlinx.datetime.Instant.fromEpochMilliseconds(dateMillis).toLocalDateTime(zone).date
val normalizedStart = kotlinx.datetime.Instant.fromEpochMilliseconds(startMillis).toLocalDateTime(zone).date
val normalizedEnd = kotlinx.datetime.Instant.fromEpochMilliseconds(end).toLocalDateTime(zone).date

return normalizedDate >= normalizedStart && normalizedDate <= normalizedEnd
return normalizedDate in normalizedStart..normalizedEnd
}

fun LocalDate.toMonthYearString(): String {
val formatter = SimpleDateFormat(DatePattern.MONTH_YEAR_FORMAT, Locale.getDefault())
fun LocalDate.toMonthYearString(locale: Locale = Locale.getDefault()): String {
val formatter = SimpleDateFormat(DatePattern.MONTH_YEAR_FORMAT, locale)
val calendar = Calendar.getInstance()
calendar.set(year, monthNumber - CalendarConstants.MONTH_INDEX_OFFSET, CalendarConstants.FIRST_DAY_OF_MONTH)
calendar.set(year, monthNumber - CalendarConstants.MONTH_INDEX_OFFSET, Constants.FIRST_DAY_OF_MONTH)
return formatter.format(calendar.time)
}

fun LocalDate.minusMonths(months: Int): LocalDate =
this.toJavaLocalDate()
.minusMonths(months.toLong())
.withDayOfMonth(1) // Always use first day of month for display
toJavaLocalDate().minusMonths(months.toLong()).withDayOfMonth(1) // Always use first day of month for display
.toKotlinLocalDate()

fun LocalDate.plusMonths(months: Int): LocalDate =
this.toJavaLocalDate()
.plusMonths(months.toLong())
.withDayOfMonth(1) // Always use first day of month for display
toJavaLocalDate().plusMonths(months.toLong()).withDayOfMonth(1) // Always use first day of month for display
.toKotlinLocalDate()

fun LocalDate.endOfDay(): Long {
return this.atStartOfDayIn(TimeZone.currentSystemDefault())
.plus(1.days)
.minus(1.milliseconds)
.toEpochMilliseconds()
}
fun LocalDate.endOfDay(zone: TimeZone = TimeZone.currentSystemDefault()): Long =
atStartOfDayIn(zone).plus(1.days).minus(1.milliseconds).toEpochMilliseconds()

fun utcDateFormatterOf(pattern: String) = SimpleDateFormat(pattern, Locale.US).apply {
timeZone = java.util.TimeZone.getTimeZone("UTC")
Expand All @@ -147,11 +186,39 @@ object DatePattern {
const val WEEKDAY_FORMAT = "EEE"
}

object CalendarConstants {
private object Constants {
// Calendar
const val FIRST_DAY_OF_MONTH = 1

// Leap year calculation
const val LEAP_YEAR_DIVISOR_4 = 4
const val LEAP_YEAR_DIVISOR_100 = 100
const val LEAP_YEAR_DIVISOR_400 = 400
}

private object Factor {
const val MILLIS_TO_SECONDS = 1000.0
const val SECONDS_TO_MINUTES = 60.0
const val MINUTES_TO_HOURS = 60.0
const val HOURS_TO_DAYS = 24.0
const val DAYS_TO_WEEKS = 7.0
const val DAYS_TO_MONTHS = 30.0
const val DAYS_TO_YEARS = 365.0
}

private object Threshold {
const val SECONDS = 60
const val MINUTES = 60
const val HOURS = 24
const val YESTERDAY = 2
const val DAYS = 7
const val WEEKS = 4
const val MONTHS = 12
}

object CalendarConstants {
// Calendar grid
const val DAYS_IN_WEEK = 7
const val FIRST_DAY_OF_MONTH = 1

// Date formatting
const val WEEKDAY_ABBREVIATION_LENGTH = 3
Expand All @@ -160,12 +227,4 @@ object CalendarConstants {
const val DAYS_IN_WEEK_MOD = 7
const val CALENDAR_WEEK_OFFSET = 1
const val MONTH_INDEX_OFFSET = 1

// Leap year calculation
const val LEAP_YEAR_DIVISOR_4 = 4
const val LEAP_YEAR_DIVISOR_100 = 100
const val LEAP_YEAR_DIVISOR_400 = 400

// Preview
const val PREVIEW_DAYS_AGO = 7
}
31 changes: 31 additions & 0 deletions app/src/main/java/to/bitkit/ext/TagMetadataEntity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package to.bitkit.ext

import com.synonym.bitkitcore.PreActivityMetadata
import to.bitkit.data.entities.TagMetadataEntity

// TODO use PreActivityMetadata
fun TagMetadataEntity.toActivityTagsMetadata() = PreActivityMetadata(
paymentId = id,
createdAt = createdAt.toULong(),
tags = tags,
paymentHash = paymentHash,
txId = txId,
address = address,
isReceive = isReceive,
feeRate = 0u, // TODO: update room db entity or drop it in favour of bitkit-core
isTransfer = false, // TODO: update room db entity or drop it in favour of bitkit-core
channelId = "", // TODO: update room db entity or drop it in favour of bitkit-core
)

fun PreActivityMetadata.toTagMetadataEntity() = TagMetadataEntity(
id = paymentId,
createdAt = createdAt.toLong(),
tags = tags,
paymentHash = paymentHash,
txId = txId,
address = address.orEmpty(),
isReceive = isReceive,
// feeRate = 0u,
// isTransfer = false,
// channelId = "",
)
6 changes: 3 additions & 3 deletions app/src/main/java/to/bitkit/models/BackupCategory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ enum class BackupCategory(
}

/**
* @property running In progress
* @property synced Timestamp in ms of last time this backup was synced
* @property required Timestamp in ms of last time this backup was required
* @property running Backup is currently in progress
* @property synced Timestamp in millis of last time this backup succeeded
* @property required Timestamp in millis of last time the data changed
*/
@Serializable
data class BackupItemStatus(
Expand Down
Loading
Loading