Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c2a51d1
refactor: replace ToastEventBus with injectable Toaster
ovitrif Jan 16, 2026
2557f16
refactor: rename toast description param to body
ovitrif Jan 16, 2026
26092e3
refactor: use Duration for toast visibility
ovitrif Jan 16, 2026
e3679ad
refactor: remove Context from Toaster
ovitrif Jan 16, 2026
32ad8c5
refactor: rename ToastQueueManager to ToastQueue
ovitrif Jan 16, 2026
301e073
refactor: make ToastQueue inherit BaseCoroutineScope
ovitrif Jan 16, 2026
0e3e7f5
docs: add *Manager anti-pattern rule
ovitrif Jan 16, 2026
96ec53c
refactor: expose Toaster via CompositionLocal
ovitrif Jan 16, 2026
f644b0a
refactor: use LocalToaster in composables
ovitrif Jan 16, 2026
20be904
refactor: rename ToastView to ToastContent
ovitrif Jan 16, 2026
3a3b96d
test: add ToastQueue behavior tests
ovitrif Jan 16, 2026
6feaede
refactor: add @StringRes overloads to Toaster
ovitrif Jan 16, 2026
1ad36fa
refactor: use @StringRes in composable toasts
ovitrif Jan 16, 2026
a9b7649
refactor: use @StringRes in ViewModel toasts
ovitrif Jan 16, 2026
84c5eee
fix: localize hardcoded toast strings
ovitrif Jan 16, 2026
f50889f
refactor: prettify Toast and Toaster APIs
ovitrif Jan 17, 2026
bb124a6
refactor: use ToastText() factory constructor
ovitrif Jan 17, 2026
fbe9826
refactor: make Toaster methods non-suspend
ovitrif Jan 17, 2026
22a0839
refactor: migrate external app.toast calls to toaster
ovitrif Jan 17, 2026
9db7c89
refactor: migrate internal AppViewModel toast calls
ovitrif Jan 17, 2026
72513b9
refactor: remove toast() methods from AppViewModel
ovitrif Jan 17, 2026
f179747
refactor: add ToastText.Parameterized
ovitrif Jan 17, 2026
ac36cd3
refactor: consolidate Toaster to single ToastText API
ovitrif Jan 17, 2026
b6a4ad2
refactor: restore ToastQueue to use CoroutineScope
ovitrif Jan 17, 2026
029e401
refactor: use composable parameter pattern for toaster
ovitrif Jan 17, 2026
50bbe71
refactor: remove toastException lambdas
ovitrif Jan 17, 2026
26f0f75
refactor: make toaster private, inject in MainActivity
ovitrif Jan 17, 2026
24658f2
feat: localize migration restart toast
ovitrif Jan 17, 2026
358ee16
refactor: restore ToastQueue provider for testability
ovitrif Jan 17, 2026
5237794
feat: localize currency rates error toast
ovitrif Jan 17, 2026
00b2273
refactor: use ToastText.Parameterized in ExternalNodeViewModel
ovitrif Jan 17, 2026
4aa5deb
refactor: clean up toast API usage
ovitrif Jan 17, 2026
e030ac2
chore: cleanup
ovitrif Jan 17, 2026
3aecbd4
Merge branch 'master' into refactor/toast
ovitrif Jan 17, 2026
281d62f
fix: pass toaster to IsOnlineTracker
ovitrif Jan 17, 2026
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,12 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
- ALWAYS add new localizable string string resources in alphabetical order in `strings.xml`
- NEVER add string resources for strings used only in dev settings screens and previews and never localize acronyms
- ALWAYS use template in `.github/pull_request_template.md` for PR descriptions
- ALWAYS review PR description after new pushes to acknowledge what changes the latest pushes warrant and update it accordingly
- ALWAYS wrap `ULong` numbers with `USat` in arithmetic operations, to guard against overflows
- PREFER to use one-liners with `run {}` when applicable, e.g. `override fun someCall(value: String) = run { this.value = value }`
- ALWAYS add imports instead of inline fully-qualified names
- PREFER to place `@Suppress()` annotations at the narrowest possible scope
- NEVER use `*Manager` suffix for classes, PREFER narrow-scope constructs that do not tend to grow into unmaintainable god objects

### Architecture Guidelines

Expand Down
10 changes: 3 additions & 7 deletions app/src/main/java/to/bitkit/di/ViewModelModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,16 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import to.bitkit.ui.shared.toast.ToastQueueManager
import to.bitkit.ui.shared.toast.ToastQueue
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ViewModelModule {
@Singleton
@Provides
fun provideFirebaseMessaging(): FirebaseMessaging {
return FirebaseMessaging.getInstance()
}
fun provideFirebaseMessaging(): FirebaseMessaging = FirebaseMessaging.getInstance()

@Provides
fun provideToastManagerProvider(): (CoroutineScope) -> ToastQueueManager {
return ::ToastQueueManager
}
fun provideToastQueueProvider(): (CoroutineScope) -> ToastQueue = ::ToastQueue
}
19 changes: 12 additions & 7 deletions app/src/main/java/to/bitkit/models/Toast.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package to.bitkit.models

import androidx.compose.runtime.Stable
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

@Stable
data class Toast(
val type: ToastType,
val title: String,
val description: String? = null,
val autoHide: Boolean,
val visibilityTime: Long = VISIBILITY_TIME_DEFAULT,
val title: ToastText,
val body: ToastText? = null,
val autoHide: Boolean = true,
val duration: Duration = DURATION_DEFAULT,
val testTag: String? = null,
) {
enum class ToastType { SUCCESS, INFO, LIGHTNING, WARNING, ERROR }

companion object {
const val VISIBILITY_TIME_DEFAULT = 3000L
val DURATION_DEFAULT: Duration = 3.seconds
}
}

enum class ToastType { SUCCESS, INFO, LIGHTNING, WARNING, ERROR }
47 changes: 47 additions & 0 deletions app/src/main/java/to/bitkit/models/ToastText.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package to.bitkit.models

import android.content.Context
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.res.stringResource

@Stable
sealed interface ToastText {
@JvmInline
value class Resource(@StringRes val resId: Int) : ToastText

data class Parameterized(
@StringRes val resId: Int,
val params: Map<String, String>,
) : ToastText

@JvmInline
value class Literal(val value: String) : ToastText

companion object {
operator fun invoke(value: String): ToastText = Literal(value)
operator fun invoke(@StringRes resId: Int): ToastText = Resource(resId)
operator fun invoke(
@StringRes resId: Int,
params: Map<String, String>,
): ToastText = Parameterized(resId, params)
}
}

@Composable
fun ToastText.asString(): String = when (this) {
is ToastText.Resource -> stringResource(resId)
is ToastText.Parameterized -> params.entries.fold(stringResource(resId)) { acc, (key, value) ->
acc.replace("{$key}", value)
}
is ToastText.Literal -> value
}

fun ToastText.asString(context: Context): String = when (this) {
is ToastText.Resource -> context.getString(resId)
is ToastText.Parameterized -> params.entries.fold(context.getString(resId)) { acc, (key, value) ->
acc.replace("{$key}", value)
}
is ToastText.Literal -> value
}
16 changes: 8 additions & 8 deletions app/src/main/java/to/bitkit/repositories/BackupRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,18 @@ import to.bitkit.data.backup.VssBackupClient
import to.bitkit.data.resetPin
import to.bitkit.di.IoDispatcher
import to.bitkit.di.json
import to.bitkit.ext.formatPlural
import to.bitkit.ext.nowMillis
import to.bitkit.models.ActivityBackupV1
import to.bitkit.models.BackupCategory
import to.bitkit.models.BackupItemStatus
import to.bitkit.models.BlocktankBackupV1
import to.bitkit.models.MetadataBackupV1
import to.bitkit.models.SettingsBackupV1
import to.bitkit.models.Toast
import to.bitkit.models.ToastText
import to.bitkit.models.WalletBackupV1
import to.bitkit.models.WidgetsBackupV1
import to.bitkit.services.LightningService
import to.bitkit.ui.shared.toast.ToastEventBus
import to.bitkit.ui.shared.toast.Toaster
import to.bitkit.utils.Logger
import to.bitkit.utils.jsonLogOf
import java.util.concurrent.ConcurrentHashMap
Expand Down Expand Up @@ -86,6 +85,7 @@ class BackupRepo @Inject constructor(
private val lightningService: LightningService,
private val clock: Clock,
private val db: AppDb,
private val toaster: Toaster,
) {
private val scope = CoroutineScope(ioDispatcher + SupervisorJob())

Expand Down Expand Up @@ -373,11 +373,11 @@ class BackupRepo @Inject constructor(
lastNotificationTime = currentTime

scope.launch {
ToastEventBus.send(
type = Toast.ToastType.ERROR,
title = context.getString(R.string.settings__backup__failed_title),
description = context.getString(R.string.settings__backup__failed_message).formatPlural(
mapOf("interval" to (BACKUP_CHECK_INTERVAL / MINUTE_IN_MS))
toaster.error(
title = ToastText(R.string.settings__backup__failed_title),
body = ToastText(
R.string.settings__backup__failed_message,
mapOf("interval" to (BACKUP_CHECK_INTERVAL / MINUTE_IN_MS).toString())
),
)
}
Expand Down
23 changes: 12 additions & 11 deletions app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import to.bitkit.R
import to.bitkit.data.CacheStore
import to.bitkit.data.SettingsStore
import to.bitkit.di.BgDispatcher
Expand All @@ -29,11 +30,11 @@ import to.bitkit.models.FxRate
import to.bitkit.models.PrimaryDisplay
import to.bitkit.models.SATS_IN_BTC
import to.bitkit.models.STUB_RATE
import to.bitkit.models.Toast
import to.bitkit.models.ToastText
import to.bitkit.models.asBtc
import to.bitkit.models.formatCurrency
import to.bitkit.services.CurrencyService
import to.bitkit.ui.shared.toast.ToastEventBus
import to.bitkit.ui.shared.toast.Toaster
import to.bitkit.utils.Logger
import java.math.BigDecimal
import java.math.RoundingMode
Expand All @@ -44,7 +45,7 @@ import kotlin.time.Clock
import kotlin.time.ExperimentalTime

@OptIn(ExperimentalTime::class)
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LongParameterList")
@Singleton
class CurrencyRepo @Inject constructor(
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
Expand All @@ -53,6 +54,7 @@ class CurrencyRepo @Inject constructor(
private val cacheStore: CacheStore,
private val clock: Clock,
@Named("enablePolling") private val enablePolling: Boolean,
private val toaster: Toaster,
) : AmountInputHandler {
private val repoScope = CoroutineScope(bgDispatcher + SupervisorJob())
private val _currencyState = MutableStateFlow(CurrencyState())
Expand Down Expand Up @@ -92,10 +94,9 @@ class CurrencyRepo @Inject constructor(
.distinctUntilChanged()
.collect { isStale ->
if (isStale) {
ToastEventBus.send(
type = Toast.ToastType.ERROR,
title = "Rates currently unavailable",
description = "An error has occurred. Please try again later."
toaster.error(
title = ToastText(R.string.currency__rates_error_title),
body = ToastText(R.string.currency__rates_error_body),
)
}
}
Expand All @@ -107,18 +108,18 @@ class CurrencyRepo @Inject constructor(
combine(
settingsStore.data.distinctUntilChanged(),
cacheStore.data.distinctUntilChanged()
) { settings, cachedData ->
val selectedRate = cachedData.cachedRates.firstOrNull { rate ->
) { settings, cache ->
val selectedRate = cache.cachedRates.firstOrNull { rate ->
rate.quote == settings.selectedCurrency
}
_currencyState.value.copy(
rates = cachedData.cachedRates,
rates = cache.cachedRates,
selectedCurrency = settings.selectedCurrency,
displayUnit = settings.displayUnit,
primaryDisplay = settings.primaryDisplay,
currencySymbol = selectedRate?.currencySymbol ?: "$",
error = null,
hasStaleData = false
hasStaleData = false,
)
}.collect { newState ->
_currencyState.update { newState }
Expand Down
15 changes: 5 additions & 10 deletions app/src/main/java/to/bitkit/services/CoreService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,7 @@ class ActivityService(
)
}

@Suppress("CyclomaticComplexMethod")
private suspend fun processOnchainPayment(
kind: PaymentKind.Onchain,
payment: PaymentDetails,
Expand All @@ -646,12 +647,7 @@ class ActivityService(
}
}

if (existingActivity != null &&
existingActivity is Activity.Onchain &&
((existingActivity as Activity.Onchain).v1.updatedAt ?: 0u) > payment.latestUpdateTimestamp
) {
return
}
if (((existingActivity as? Activity.Onchain)?.v1?.updatedAt ?: 0u) > payment.latestUpdateTimestamp) return

var resolvedChannelId = channelId

Expand All @@ -672,7 +668,7 @@ class ActivityService(
val ldkValue = payment.amountSats ?: 0u
val onChain = if (existingActivity is Activity.Onchain) {
buildUpdatedOnchainActivity(
existingActivity = existingActivity as Activity.Onchain,
existingActivity = existingActivity,
confirmationData = confirmationData,
ldkValue = ldkValue,
channelId = resolvedChannelId,
Expand All @@ -692,9 +688,8 @@ class ActivityService(
return
}

if (existingActivity != null && existingActivity is Activity.Onchain) {
val existingOnchain = existingActivity.v1
updateActivity(existingOnchain.id, Activity.Onchain(onChain))
if (existingActivity is Activity.Onchain) {
updateActivity(existingActivity.v1.id, Activity.Onchain(onChain))
} else {
upsertActivity(Activity.Onchain(onChain))
}
Expand Down
26 changes: 11 additions & 15 deletions app/src/main/java/to/bitkit/services/MigrationService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1394,7 +1394,7 @@ class MigrationService @Inject constructor(
}
}

@Suppress("CyclomaticComplexMethod")
@Suppress("CyclomaticComplexMethod", "LongMethod")
private suspend fun updateOnchainActivityMetadata(
item: RNActivityItem,
onchain: OnchainActivity,
Expand Down Expand Up @@ -1443,9 +1443,9 @@ class MigrationService @Inject constructor(
wasUpdated = true
}

item.feeRate?.let { feeRate ->
if (feeRate > 0 && updated.feeRate != feeRate.toULong()) {
updated = updated.copy(feeRate = feeRate.toULong())
item.feeRate?.toULong()?.let { feeRate ->
if (feeRate > 0u && updated.feeRate != feeRate) {
updated = updated.copy(feeRate = feeRate)
wasUpdated = true
}
}
Expand Down Expand Up @@ -1487,12 +1487,8 @@ class MigrationService @Inject constructor(
updateOnchainActivityMetadata(item, onchain)?.let { updated ->
activityRepo.updateActivity(updated.id, Activity.Onchain(updated))
.onSuccess { updatedCount++ }
.onFailure { e ->
Logger.error(
"Failed to update onchain activity metadata for $txId: $e",
e,
context = TAG
)
.onFailure {
Logger.error("Failed to update onchain activity metadata for $txId", it, context = TAG)
}
}
} else {
Expand Down Expand Up @@ -1531,11 +1527,11 @@ class MigrationService @Inject constructor(
applyBoostedParents(parents, txId)
}
}
.onFailure { e ->
.onFailure {
Logger.error(
"Failed to create onchain activity for unsupported address $txId: $e",
e,
context = TAG
"Failed to create onchain activity for unsupported address $txId",
it,
context = TAG,
)
}
}
Expand All @@ -1544,7 +1540,7 @@ class MigrationService @Inject constructor(
if (updatedCount > 0 || createdCount > 0) {
Logger.info(
"Applied metadata to $updatedCount onchain activities, created $createdCount for unsupported addresses",
context = TAG
context = TAG,
)
}
}
Expand Down
12 changes: 3 additions & 9 deletions app/src/main/java/to/bitkit/ui/ContentView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import to.bitkit.env.Env
import to.bitkit.models.NodeLifecycleState
import to.bitkit.models.Toast
import to.bitkit.models.WidgetType
import to.bitkit.ui.Routes.ExternalConnection
import to.bitkit.ui.components.AuthCheckScreen
Expand Down Expand Up @@ -167,6 +166,7 @@ import to.bitkit.ui.settings.support.ReportIssueScreen
import to.bitkit.ui.settings.support.SupportScreen
import to.bitkit.ui.settings.transactionSpeed.CustomFeeSettingsScreen
import to.bitkit.ui.settings.transactionSpeed.TransactionSpeedSettingsScreen
import to.bitkit.ui.shared.toast.Toaster
import to.bitkit.ui.sheets.BackgroundPaymentsIntroSheet
import to.bitkit.ui.sheets.BackupRoute
import to.bitkit.ui.sheets.BackupSheet
Expand Down Expand Up @@ -209,6 +209,7 @@ fun ContentView(
transferViewModel: TransferViewModel,
settingsViewModel: SettingsViewModel,
backupsViewModel: BackupsViewModel,
toaster: Toaster,
hazeState: HazeState,
modifier: Modifier = Modifier,
) {
Expand Down Expand Up @@ -360,6 +361,7 @@ fun ContentView(
LocalTransferViewModel provides transferViewModel,
LocalSettingsViewModel provides settingsViewModel,
LocalBackupsViewModel provides backupsViewModel,
LocalToaster provides toaster,
LocalDrawerState provides drawerState,
LocalBalances provides balance,
LocalCurrencies provides currencies,
Expand Down Expand Up @@ -625,14 +627,6 @@ private fun RootNavHost(
viewModel = transferViewModel,
onBackClick = { navController.popBackStack() },
onOrderCreated = { navController.navigate(Routes.SpendingConfirm) },
toastException = { appViewModel.toast(it) },
toast = { title, description ->
appViewModel.toast(
type = Toast.ToastType.ERROR,
title = title,
description = description
)
},
)
}
composableWithDefaultTransitions<Routes.SpendingConfirm> {
Expand Down
Loading
Loading